Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(inputs): detect constraint validation attribute changes #930

Merged
Merged
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 101 additions & 5 deletions packages/oruga/src/composables/useInputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
type ExtractPropTypes,
type MaybeRefOrGetter,
type Component,
watch,
} from "vue";
import { injectField } from "@/components/field/fieldInjection";
import { unrefElement } from "./unrefElement";
Expand Down Expand Up @@ -35,6 +36,17 @@
: null;
}

const constraintValidationAttributes = [
"disabled",
"required",
"pattern",
"maxlength",
"minlength",
"max",
"min",
"step",
];
mlmoravek marked this conversation as resolved.
Show resolved Hide resolved

/**
* Form input handler functionalities
*/
Expand All @@ -61,10 +73,9 @@
// inject parent field component if used inside one
const { parentField } = injectField();

const element = computed<ValidatableFormElement>(() => {
const maybeElement = computed<ValidatableFormElement | undefined>(() => {
const el = unrefElement<Component | HTMLElement>(inputRef);
if (!el) {
console.warn("useInputHandler: inputRef contains no element");
mlmoravek marked this conversation as resolved.
Show resolved Hide resolved
return undefined;
}
if (el.getAttribute("data-oruga-input"))
Expand All @@ -83,6 +94,14 @@
return inputs as ValidatableFormElement;
});

const element = computed(() => {
const el = maybeElement.value;
if (!el) {
console.warn("useInputHandler: inputRef contains no element");

Check warning on line 100 in packages/oruga/src/composables/useInputHandler.ts

View check run for this annotation

Codecov / codecov/patch

packages/oruga/src/composables/useInputHandler.ts#L100

Added line #L100 was not covered by tests
}
return el;
});

// --- Input Focus Feature ---

const isFocused = ref(false);
Expand Down Expand Up @@ -138,7 +157,7 @@
* If validation fail, send 'danger' type,
* and error message to parent if it's a Field.
*/
function checkHtml5Validity(): boolean {
function checkHtml5Validity(): void {
if (!props.useHtml5Validation) return;

if (!element.value) return;
Expand All @@ -149,8 +168,6 @@
setInvalid();
isValid.value = false;
}

return isValid.value;
mlmoravek marked this conversation as resolved.
Show resolved Hide resolved
}

function setInvalid(): void {
Expand Down Expand Up @@ -203,6 +220,85 @@
emits("invalid", event);
}

if (!isSSR) {
// Respond to attribute changes that might clear constraint validation errors.
// For instance, removing the `required` attribute on an empty field means that it's no
// longer invalid, so we might as well clear the validation message.
// In order to follow our usual convention, we won't add new validation messages
// until the next time the user interacts with the control.

// Technically, having the `required` attribute on one element in a radio button
// group affects the validity of the entire group.
// See https://html.spec.whatwg.org/multipage/input.html#radio-button-group.
// We're not checking for that here because it would require more expensive logic.
// Because of that, this will only work properly if the `required` attributes of all radio
// buttons in the group are synchronized with each other, which is likely anyway.
// (We're also expecting the use of radio buttons with our default validation message handling
// to be fairly uncommon because the overall visual experience is clunky with such a configuration.)
const onAttributeChange = (): void => {
if (!isValid.value) checkHtml5Validity();
mlmoravek marked this conversation as resolved.
Show resolved Hide resolved
};
let validationAttributeObserver: MutationObserver | null = null;
let ancestorMutationObserver: MutationObserver | null = null;
watch(
[maybeElement, isValid, () => props.useHtml5Validation],
(data) => {
// Not using destructuring assignment because browser support is just a little too weak at the moment
const el = data[0];
const valid = data[1];
const useValidation = data[2];

// Clean up previous state.
if (validationAttributeObserver != null) {
// Process any pending events.
if (validationAttributeObserver.takeRecords().length > 0) {
onAttributeChange();

Check warning on line 255 in packages/oruga/src/composables/useInputHandler.ts

View check run for this annotation

Codecov / codecov/patch

packages/oruga/src/composables/useInputHandler.ts#L255

Added line #L255 was not covered by tests
}
validationAttributeObserver.disconnect();

Check warning on line 257 in packages/oruga/src/composables/useInputHandler.ts

View check run for this annotation

Codecov / codecov/patch

packages/oruga/src/composables/useInputHandler.ts#L257

Added line #L257 was not covered by tests
}
if (ancestorMutationObserver != null) {
if (ancestorMutationObserver.takeRecords().length > 0) {
onAttributeChange();

Check warning on line 261 in packages/oruga/src/composables/useInputHandler.ts

View check run for this annotation

Codecov / codecov/patch

packages/oruga/src/composables/useInputHandler.ts#L261

Added line #L261 was not covered by tests
}
ancestorMutationObserver.disconnect();

Check warning on line 263 in packages/oruga/src/composables/useInputHandler.ts

View check run for this annotation

Codecov / codecov/patch

packages/oruga/src/composables/useInputHandler.ts#L263

Added line #L263 was not covered by tests
}

if (el == undefined || valid || !useValidation) {
mlmoravek marked this conversation as resolved.
Show resolved Hide resolved
return;
}

if (validationAttributeObserver == null) {
validationAttributeObserver = new MutationObserver(

Check warning on line 271 in packages/oruga/src/composables/useInputHandler.ts

View check run for this annotation

Codecov / codecov/patch

packages/oruga/src/composables/useInputHandler.ts#L271

Added line #L271 was not covered by tests
onAttributeChange,
);
}
validationAttributeObserver.observe(el, {

Check warning on line 275 in packages/oruga/src/composables/useInputHandler.ts

View check run for this annotation

Codecov / codecov/patch

packages/oruga/src/composables/useInputHandler.ts#L275

Added line #L275 was not covered by tests
attributeFilter: constraintValidationAttributes,
});

// Note that this doesn't react to changes in the list of ancestors.
// Based on testing, Vue seems to rarely, if ever, re-parent DOM nodes;
// it generally prefers to create new ones under the new parent.
// That means this simpler solution is likely good enough for now.
let ancestor: Node | null = el;
while ((ancestor = ancestor.parentNode)) {

Check warning on line 284 in packages/oruga/src/composables/useInputHandler.ts

View check run for this annotation

Codecov / codecov/patch

packages/oruga/src/composables/useInputHandler.ts#L283-L284

Added lines #L283 - L284 were not covered by tests
// Form controls can be disabled by their ancestor fieldsets.
if (ancestor instanceof HTMLFieldSetElement) {
if (ancestorMutationObserver == null) {
ancestorMutationObserver = new MutationObserver(

Check warning on line 288 in packages/oruga/src/composables/useInputHandler.ts

View check run for this annotation

Codecov / codecov/patch

packages/oruga/src/composables/useInputHandler.ts#L288

Added line #L288 was not covered by tests
mlmoravek marked this conversation as resolved.
Show resolved Hide resolved
onAttributeChange,
);
}
ancestorMutationObserver.observe(ancestor, {

Check warning on line 292 in packages/oruga/src/composables/useInputHandler.ts

View check run for this annotation

Codecov / codecov/patch

packages/oruga/src/composables/useInputHandler.ts#L292

Added line #L292 was not covered by tests
attributeFilter: ["disabled"],
});
}
}
},
{ immediate: true },
);
}

return {
isFocused,
isValid,
Expand Down