fixing validation

This commit is contained in:
konnorrogers
2024-05-08 14:24:11 -04:00
parent 34bba3db19
commit e6a4eadf1c
12 changed files with 96 additions and 147 deletions

View File

@@ -31,7 +31,7 @@
{# Component API #}
{% block afterContent %}
{# Slots #}
{% if component.slots.length %}
{% if component.slots?.length %}
<h2>Slots</h2>
<div class="table-scroll">
@@ -61,7 +61,7 @@
{% endif %}
{# Properties #}
{% if component.properties.length %}
{% if component.properties?.length %}
<h2>Properties</h2>
<div class="table-scroll">
@@ -113,7 +113,7 @@
{% endif %}
{# Methods #}
{% if component.methods.length %}
{% if component.methods?.length %}
<h2>Methods</h2>
<div class="table-scroll">
<table class="component-table">
@@ -130,7 +130,7 @@
<td class="table-name"><code>{{ method.name }}()</code></td>
<td class="table-description">{{ method.description | inlineMarkdown | safe }}</td>
<td class="table-arguments">
{% if method.parameters.length %}
{% if method.parameters?.length %}
<code>
{% for param in method.parameters %}
{{ param.name }}: {{ param.type.text | trimPipes }}{% if not loop.last %},{% endif %}
@@ -146,7 +146,7 @@
{% endif %}
{# States #}
{% if component.states.length %}
{% if component.states?.length %}
<h2>States</h2>
<div class="table-scroll">
<table class="component-table">
@@ -171,7 +171,7 @@
{% endif %}
{# Events #}
{% if component.events.length %}
{% if component.events?.length %}
<h2>Events</h2>
<div class="table-scroll">
@@ -195,7 +195,7 @@
{% endif %}
{# Custom Properties #}
{% if component.cssProperties.length %}
{% if component.cssProperties?.length %}
<h2>CSS custom properties</h2>
<div class="table-scroll">
@@ -225,7 +225,7 @@
{% endif %}
{# CSS Parts #}
{% if component.cssParts.length %}
{% if component.cssParts?.length %}
<h2>CSS parts</h2>
<div class="table-scroll">
@@ -251,7 +251,7 @@
{% endif %}
{# Dependencies #}
{% if component.dependencies.length %}
{% if component.dependencies?.length %}
<h2>Dependencies</h2>
<p>
This component automatically imports the following elements. Subdependencies, if any exist, will also be included in this list.

View File

@@ -244,7 +244,7 @@ Use the `setCustomValidity()` method to set a custom validation message. This wi
const errorMessage = 'You must choose the last option';
// Set initial validity as soon as the element is defined
customElements.whenDefined('wa-radio').then(() => {
customElements.whenDefined('wa-radio-group').then(() => {
radioGroup.setCustomValidity(errorMessage);
});
@@ -301,4 +301,4 @@ const App = () => {
);
};
```
{% endraw %}
{% endraw %}

View File

@@ -1,18 +1,17 @@
import '../icon/icon.js';
import '../spinner/spinner.js';
import { classMap } from 'lit/directives/class-map.js';
import { customElement, property, query, state } from 'lit/decorators.js';
import { FormControlController, validValidityState } from '../../internal/form.js';
import { HasSlotController } from '../../internal/slot.js';
import { html, literal } from 'lit/static-html.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { LocalizeController } from '../../utilities/localize.js';
import { MirrorValidator } from '../../internal/validators/mirror-validator.js';
import { customElement, property, query, state } from 'lit/decorators.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociated } from '../../internal/webawesome-element.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './button.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { WebAwesomeFormControl } from '../../internal/webawesome-element.js';
/**
* @summary Buttons represent actions that are available to the user.
@@ -53,13 +52,17 @@ import type { WebAwesomeFormControl } from '../../internal/webawesome-element.js
* @cssproperty --label-color-active - The color of the button's label when active.
* @cssproperty --label-color-hover - The color of the button's label on hover.
*/
@customElement('wa-button')
export default class WaButton extends WebAwesomeElement implements WebAwesomeFormControl {
@customElement("wa-button")
export default class WaButton extends WebAwesomeFormAssociated {
static styles: CSSResultGroup = [componentStyles, styles];
private readonly formControlController = new FormControlController(this, {
assumeInteractionOn: ['click']
});
static get validators () {
return [
MirrorValidator()
]
}
assumeInteractionOn = ["click"]
private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
private readonly localize = new LocalizeController(this);
@@ -79,7 +82,7 @@ export default class WaButton extends WebAwesomeElement implements WebAwesomeFor
@property({ type: Boolean, reflect: true }) caret = false;
/** Disables the button. */
@property({ type: Boolean, reflect: true }) disabled = false;
@property({ type: Boolean }) disabled = false;
/** Draws the button in a loading state. */
@property({ type: Boolean, reflect: true }) loading = false;
@@ -106,7 +109,7 @@ export default class WaButton extends WebAwesomeElement implements WebAwesomeFor
* The value of the button, submitted as a pair with the button's name as part of the form data, but only when this
* button is the submitter. This attribute is ignored when `href` is present.
*/
@property() value = '';
@property({ reflect: true }) value = '';
/** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */
@property() href = '';
@@ -129,7 +132,7 @@ export default class WaButton extends WebAwesomeElement implements WebAwesomeFor
* The "form owner" to associate the button with. If omitted, the closest containing form will be used instead. The
* value of this attribute must be an id of a form in the same document or shadow root as the button.
*/
@property() form: string;
@property({ reflect: true }) form: string | null = null
/** Used to override the form owner's `action` attribute. */
@property({ attribute: 'formaction' }) formAction: string;
@@ -147,30 +150,6 @@ export default class WaButton extends WebAwesomeElement implements WebAwesomeFor
/** Used to override the form owner's `target` attribute. */
@property({ attribute: 'formtarget' }) formTarget: '_self' | '_blank' | '_parent' | '_top' | string;
/** Gets the validity state object */
get validity() {
if (this.isButton()) {
return (this.button as HTMLButtonElement).validity;
}
return validValidityState;
}
/** Gets the validation message */
get validationMessage() {
if (this.isButton()) {
return (this.button as HTMLButtonElement).validationMessage;
}
return '';
}
firstUpdated() {
if (this.isButton()) {
this.formControlController.updateValidity();
}
}
private handleBlur() {
this.hasFocus = false;
this.emit('wa-blur');
@@ -182,18 +161,41 @@ export default class WaButton extends WebAwesomeElement implements WebAwesomeFor
}
private handleClick() {
if (this.type === 'submit') {
this.formControlController.submit(this);
}
const form = this.getForm()
if (this.type === 'reset') {
this.formControlController.reset(this);
}
if (!form) return
const lightDOMButton = this.constructLightDOMButton()
// form.append(lightDOMButton);
this.parentElement?.append(lightDOMButton)
lightDOMButton.click();
lightDOMButton.remove();
}
private handleInvalid(event: Event) {
this.formControlController.setValidity(false);
this.formControlController.emitInvalidEvent(event);
private constructLightDOMButton () {
const button = document.createElement('button');
button.type = this.type;
button.style.position = 'absolute';
button.style.width = '0';
button.style.height = '0';
button.style.clipPath = 'inset(50%)';
button.style.overflow = 'hidden';
button.style.whiteSpace = 'nowrap';
button.name = this.name;
button.value = this.value;
['form', 'formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget'].forEach(attr => {
if (this.hasAttribute(attr)) {
button.setAttribute(attr, this.getAttribute(attr)!);
}
});
return button
}
private handleInvalid() {
this.emit("wa-invalid");
}
private isButton() {
@@ -206,10 +208,13 @@ export default class WaButton extends WebAwesomeElement implements WebAwesomeFor
@watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() {
if (this.isButton()) {
// Disabled form controls are always valid
this.formControlController.setValidity(this.disabled);
}
this.updateValidity()
}
// eslint-disable-next-line
setValue (..._args: Parameters<WebAwesomeFormAssociated["setValue"]>) {
// This is just a stub. We dont ever actually want to set a value on the form. That happens when the button is clicked and added
// via the light dom button.
}
/** Simulates a click on the button. */
@@ -227,37 +232,6 @@ export default class WaButton extends WebAwesomeElement implements WebAwesomeFor
this.button.blur();
}
/** Checks for validity but does not show a validation message. Returns `true` when valid and `false` when invalid. */
checkValidity() {
if (this.isButton()) {
return (this.button as HTMLButtonElement).checkValidity();
}
return true;
}
/** Gets the associated form, if one exists. */
getForm(): HTMLFormElement | null {
return this.formControlController.getForm();
}
/** Checks for validity and shows the browser's validation message if the control is invalid. */
reportValidity() {
if (this.isButton()) {
return (this.button as HTMLButtonElement).reportValidity();
}
return true;
}
/** Sets a custom validation message. Pass an empty string to restore validity. */
setCustomValidity(message: string) {
if (this.isButton()) {
(this.button as HTMLButtonElement).setCustomValidity(message);
this.formControlController.updateValidity();
}
}
render() {
const isLink = this.isLink();
const tag = isLink ? literal`a` : literal`button`;
@@ -330,7 +304,6 @@ export default class WaButton extends WebAwesomeElement implements WebAwesomeFor
/* eslint-enable lit/binding-positions */
}
}
declare global {
interface HTMLElementTagNameMap {
'wa-button': WaButton;

View File

@@ -783,7 +783,6 @@ export default class WaColorPicker extends WebAwesomeFormAssociated {
if (!this.disabled) {
// By standards we have to emit a `wa-invalid` event here synchronously.
// this.formControlController.emitInvalidEvent();
this.emit('wa-invalid');
}
@@ -793,18 +792,8 @@ export default class WaColorPicker extends WebAwesomeFormAssociated {
return super.reportValidity();
}
formStateRestoreCallback(...args: Parameters<WebAwesomeFormAssociated['formStateRestoreCallback']>) {
const [value, reason] = args;
const oldValue = this.value;
super.formStateRestoreCallback(value, reason);
this.handleValueChange(oldValue, this.value);
}
formResetCallback() {
const oldValue = this.value;
this.value = this.defaultValue;
this.handleValueChange(oldValue, this.value);
super.formResetCallback();
}

View File

@@ -374,9 +374,6 @@ describe('<wa-input>', () => {
</form>
`);
// Yes, this is a "breakage" from previous overloads, but this is how the browser works :shrug:
// https://codepen.io/paramagicdev/pen/rNbpqje
expect(form.reportValidity()).to.be.false;
});
@@ -582,7 +579,7 @@ describe('<wa-input>', () => {
it('Should be invalid if the pattern is invalid', async () => {
const el = await fixture<WaInput>(html` <wa-input required pattern="1234"></wa-input> `);
el.formControl.focus();
el.input.focus();
await el.updateComplete;
expect(el.checkValidity()).to.be.false;

View File

@@ -72,8 +72,6 @@ export default class WaInput extends WebAwesomeFormAssociated {
@state() private hasFocus = false;
@property() title = ''; // make reactive to pass through
private __numberInput = Object.assign(document.createElement('input'), { type: 'number' });
private __dateInput = Object.assign(document.createElement('input'), { type: 'date' });
/**
* The type of input. Works the same as a native `<input>` element, but only a subset of types are supported. Defaults
@@ -196,6 +194,9 @@ export default class WaInput extends WebAwesomeFormAssociated {
})
spellcheck = true;
private __numberInput = Object.assign(document.createElement('input'), { type: 'number' });
private __dateInput = Object.assign(document.createElement('input'), { type: 'date' });
/**
* Tells the browser what type of data will be entered by the user, allowing it to display the appropriate virtual
* keyboard on supportive devices.
@@ -359,16 +360,7 @@ export default class WaInput extends WebAwesomeFormAssociated {
}
}
formStateRestoreCallback(...args: Parameters<WebAwesomeFormAssociated['formStateRestoreCallback']>) {
const [value, reason] = args;
super.formStateRestoreCallback(value, reason);
/** @ts-expect-error Type widening issue due to what a formStateRestoreCallback can accept. */
this.input.value = value;
}
formResetCallback() {
this.input.value = this.defaultValue;
this.value = this.defaultValue;
super.formResetCallback();

View File

@@ -8,7 +8,7 @@ import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociated } from '../../internal/webawesome-element.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './radio-button.styles.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary Radios buttons allow the user to select a single option from a group using a button-like control.
@@ -46,10 +46,9 @@ export default class WaRadioButton extends WebAwesomeFormAssociated {
* it easier to style in button groups.
*/
@property({ type: Boolean, reflect: true }) checked = false;
@property({ type: Boolean, attribute: 'default-checked' }) defaultChecked = false;
/** The radio's value. When selected, the radio group will receive this value. */
@property({ attribute: false }) value: string;
@property({ reflect: true }) value: string;
/** Disables the radio button. */
@property({ type: Boolean }) disabled = false;
@@ -96,6 +95,12 @@ export default class WaRadioButton extends WebAwesomeFormAssociated {
this.emit('wa-focus');
}
// protected willUpdate(changedProperties: PropertyValues<this>): void {
// if (this.disabled && changedProperties.has("checked")) {
// this.checked = Boolean(changedProperties.get("checked"))
// }
// }
@watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() {
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');

View File

@@ -224,14 +224,14 @@ export default class WaRadioGroup extends WebAwesomeFormAssociated {
return;
}
event.preventDefault();
const radios = this.getAllRadios().filter(radio => !radio.disabled);
if (radios.length <= 0) {
return;
}
event.preventDefault();
const oldValue = this.value;
const checkedRadio = radios.find(radio => radio.checked) ?? radios[0];

View File

@@ -231,17 +231,7 @@ export default class WaRange extends WebAwesomeFormAssociated {
}
}
formStateRestoreCallback(...args: Parameters<WebAwesomeFormAssociated['formStateRestoreCallback']>) {
const [value, reason] = args;
super.formStateRestoreCallback(value, reason);
/** @ts-expect-error Type widening issue due to what a formStateRestoreCallback can accept. */
this.input.value = value;
}
formResetCallback() {
// @ts-expect-error We pass a Number, but it wants a string :shrug:
this.input.value = this.defaultValue;
this.value = this.defaultValue;
super.formResetCallback();

View File

@@ -264,16 +264,7 @@ export default class WaTextarea extends WebAwesomeFormAssociated {
}
}
formStateRestoreCallback(...args: Parameters<WebAwesomeFormAssociated['formStateRestoreCallback']>) {
const [value, reason] = args;
super.formStateRestoreCallback(value, reason);
/** @ts-expect-error Type widening issue due to what a formStateRestoreCallback can accept. */
this.input.value = value;
}
formResetCallback() {
this.input.value = this.defaultValue;
this.value = this.defaultValue;
super.formResetCallback();

View File

@@ -7,7 +7,7 @@ import type { Validator } from '../webawesome-element.js';
export const MirrorValidator = (): Validator => {
return {
checkValidity(element) {
const formControl = element.formControl;
const formControl = element.input;
const validity: ReturnType<Validator['checkValidity']> = {
message: '',

View File

@@ -182,7 +182,7 @@ export class WebAwesomeFormAssociated
assumeInteractionOn: string[] = ['wa-input'];
// Additional
formControl?: (HTMLElement & { value: unknown }) | HTMLInputElement | HTMLTextAreaElement;
input?: (HTMLElement & { value: unknown }) | HTMLInputElement | HTMLTextAreaElement;
validators: Validator[] = [];
@@ -244,11 +244,23 @@ export class WebAwesomeFormAssociated
}
}
if (changedProperties.has('value')) {
if (
changedProperties.has('value') ||
changedProperties.has("disabled")
) {
// this is a hack because of how "disabled" attribute can be set by static HTML, but then changed via property, but we don't
// want to use reflection because of a bug in "formDisabledCallback"
if (!this.disabled) { this.removeAttribute("disabled") }
if (this.hasInteracted && this.value !== this.defaultValue) {
this.valueHasChanged = true;
}
if (this.input) {
this.input.value = this.value
}
const value = this.value;
// Accounts for the snowflake case on `<wa-select>`
@@ -316,7 +328,7 @@ export class WebAwesomeFormAssociated
* Override this to change where constraint validation popups are anchored.
*/
get validationTarget(): undefined | HTMLElement {
return (this.formControl || undefined) as undefined | HTMLElement;
return (this.input || undefined) as undefined | HTMLElement;
}
setValidity(...args: Parameters<typeof this.internals.setValidity>) {
@@ -434,7 +446,7 @@ export class WebAwesomeFormAssociated
customError: Boolean(this.__manualCustomError)
};
const formControl = this.validationTarget || this.formControl || undefined;
const formControl = this.validationTarget || this.input || undefined;
let finalMessage = '';