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 5 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
95 changes: 90 additions & 5 deletions packages/oruga/src/composables/useInputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
type ExtractPropTypes,
type MaybeRefOrGetter,
type Component,
watch,
} from "vue";
import { injectField } from "@/components/field/fieldInjection";
import { unrefElement } from "./unrefElement";
import { getOption } from "@/utils/config";
import { isSSR } from "@/utils/ssr";
import { isDefined } from "@/utils/helpers";

// This should cover all types of HTML elements that have properties related to
// HTML constraint validation, e.g. .form and .validity.
Expand All @@ -35,6 +37,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 +74,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 +95,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 101 in packages/oruga/src/composables/useInputHandler.ts

View check run for this annotation

Codecov / codecov/patch

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

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

// --- Input Focus Feature ---

const isFocused = ref(false);
Expand Down Expand Up @@ -138,7 +158,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 +169,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 +221,73 @@
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;
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 (!isDefined(el) || valid || !useValidation) {
return;
}

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

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

View check run for this annotation

Codecov / codecov/patch

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

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

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L269 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 278 in packages/oruga/src/composables/useInputHandler.ts

View check run for this annotation

Codecov / codecov/patch

packages/oruga/src/composables/useInputHandler.ts#L277-L278

Added lines #L277 - L278 were not covered by tests
// Form controls can be disabled by their ancestor fieldsets.
if (ancestor instanceof HTMLFieldSetElement) {
validationAttributeObserver.observe(ancestor, {

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

View check run for this annotation

Codecov / codecov/patch

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

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

return {
isFocused,
isValid,
Expand Down