diff --git a/src/components/input/input.component.ts b/src/components/input/input.component.ts index e83ce97ed..0ee0266ac 100644 --- a/src/components/input/input.component.ts +++ b/src/components/input/input.component.ts @@ -1,20 +1,19 @@ import { classMap } from 'lit/directives/class-map.js'; import { defaultValue } from '../../internal/default-value.js'; -import { FormControlController } from '../../internal/form.js'; import { HasSlotController } from '../../internal/slot.js'; import { html } from 'lit'; import { ifDefined } from 'lit/directives/if-defined.js'; import { live } from 'lit/directives/live.js'; import { LocalizeController } from '../../utilities/localize.js'; +import { MirrorValidator } from '../../internal/validators/mirror-validator.js'; import { 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 formControlStyles from '../../styles/form-control.styles.js'; import styles from './input.styles.js'; import WaIcon from '../icon/icon.component.js'; -import WebAwesomeElement from '../../internal/webawesome-element.js'; import type { CSSResultGroup } from 'lit'; -import type { WebAwesomeFormControl } from '../../internal/webawesome-element.js'; /** * @summary Inputs collect data from the user. @@ -57,17 +56,23 @@ import type { WebAwesomeFormControl } from '../../internal/webawesome-element.js * @cssproperty --border-width - The width of the input's borders. Expects a single value. * @cssproperty --box-shadow - The shadow effects around the edges of the input. */ -export default class WaInput extends WebAwesomeElement implements WebAwesomeFormControl { +export default class WaInput extends WebAwesomeFormAssociated { static styles: CSSResultGroup = [componentStyles, formControlStyles, styles]; static dependencies = { 'wa-icon': WaIcon }; + static formAssociated = true - private readonly formControlController = new FormControlController(this, { - assumeInteractionOn: ['wa-blur', 'wa-input'] - }); + static get validators () { + return [ + MirrorValidator + ] + } + + assumeInteractionOn = ['wa-blur', 'wa-input'] private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label'); private readonly localize = new LocalizeController(this); @query('.input__control') input: HTMLInputElement; + @query('.input__control') formControl: HTMLInputElement; @state() private hasFocus = false; @property() title = ''; // make reactive to pass through @@ -119,7 +124,7 @@ export default class WaInput extends WebAwesomeElement implements WebAwesomeForm @property({ type: Boolean }) clearable = false; /** Disables the input. */ - @property({ type: Boolean, reflect: true }) disabled = false; + @property({ type: Boolean }) disabled = false; /** Placeholder text to show as a hint when the input is empty. */ @property() placeholder = ''; @@ -141,7 +146,7 @@ export default class WaInput extends WebAwesomeElement implements WebAwesomeForm * to place the form control outside of a form and associate it with the form that has this `id`. The form must be in * the same document or shadow root for this to work. */ - @property({ reflect: true }) form = ''; + @property({ reflect: true }) form = null /** Makes the input a required field. */ @property({ type: Boolean, reflect: true }) required = false; @@ -233,18 +238,8 @@ export default class WaInput extends WebAwesomeElement implements WebAwesomeForm this.value = this.__numberInput.value; } - /** Gets the validity state object */ - get validity() { - return this.input.validity; - } - - /** Gets the validation message */ - get validationMessage() { - return this.input.validationMessage; - } - firstUpdated() { - this.formControlController.updateValidity(); + this.checkValidity() } private handleBlur() { @@ -274,15 +269,9 @@ export default class WaInput extends WebAwesomeElement implements WebAwesomeForm private handleInput() { this.value = this.input.value; - this.formControlController.updateValidity(); this.emit('wa-input'); } - private handleInvalid(event: Event) { - this.formControlController.setValidity(false); - this.formControlController.emitInvalidEvent(event); - } - private handleKeyDown(event: KeyboardEvent) { const hasModifier = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey; @@ -297,7 +286,7 @@ export default class WaInput extends WebAwesomeElement implements WebAwesomeForm // See https://github.com/shoelace-style/shoelace/pull/988 // if (!event.defaultPrevented && !event.isComposing) { - this.formControlController.submit(); + this.getForm()?.requestSubmit(null) } }); } @@ -307,24 +296,12 @@ export default class WaInput extends WebAwesomeElement implements WebAwesomeForm this.passwordVisible = !this.passwordVisible; } - @watch('disabled', { waitUntilFirstUpdate: true }) - handleDisabledChange() { - // Disabled form controls are always valid - this.formControlController.setValidity(this.disabled); - } - @watch('step', { waitUntilFirstUpdate: true }) handleStepChange() { // If step changes, the value may become invalid so we need to recheck after the update. We set the new step // imperatively so we don't have to wait for the next render to report the updated validity. this.input.step = String(this.step); - this.formControlController.updateValidity(); - } - - @watch('value', { waitUntilFirstUpdate: true }) - async handleValueChange() { - await this.updateComplete; - this.formControlController.updateValidity(); + this.runValidators() } /** Sets focus on the input. */ @@ -391,27 +368,6 @@ export default class WaInput extends WebAwesomeElement implements WebAwesomeForm } } - /** Checks for validity but does not show a validation message. Returns `true` when valid and `false` when invalid. */ - checkValidity() { - return this.input.checkValidity(); - } - - /** 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() { - return this.input.reportValidity(); - } - - /** Sets a custom validation message. Pass an empty string to restore validity. */ - setCustomValidity(message: string) { - this.input.setCustomValidity(message); - this.formControlController.updateValidity(); - } - render() { const hasLabelSlot = this.hasSlotController.test('label'); const hasHelpTextSlot = this.hasSlotController.test('help-text'); @@ -494,7 +450,6 @@ export default class WaInput extends WebAwesomeElement implements WebAwesomeForm aria-describedby="help-text" @change=${this.handleChange} @input=${this.handleInput} - @invalid=${this.handleInvalid} @keydown=${this.handleKeyDown} @focus=${this.handleFocus} @blur=${this.handleBlur} diff --git a/src/components/input/input.test.ts b/src/components/input/input.test.ts index 91201d5a5..c05227d36 100644 --- a/src/components/input/input.test.ts +++ b/src/components/input/input.test.ts @@ -1,10 +1,11 @@ // eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment -import { elementUpdated, expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing'; +import { aTimeout, elementUpdated, expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing'; import { getFormControls, serialize } from '../../../dist/webawesome.js'; import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests.js'; import { sendKeys } from '@web/test-runner-commands'; // must come from the same module import sinon from 'sinon'; import type WaInput from './input.js'; +import { isSafari } from '../../internal/test.js'; describe('', () => { it('should pass accessibility tests', async () => { @@ -157,12 +158,28 @@ describe('', () => { }); it('should be invalid when required and disabled is removed', async () => { - const el = await fixture(html` `); + const el = await fixture(html` `); + // Should be valid while disabled + expect(el.checkValidity()).to.be.true; el.disabled = false; await el.updateComplete; + // Should be invalid while enabled expect(el.checkValidity()).to.be.false; }); + it('should not add a value to the form if disabled', async () => { + const form = await fixture(html`
`); + const el = form.querySelector("wa-input")! + el.value = "blah" + await el.updateComplete; + + expect(new FormData(form).get("name")).to.equal(null) + el.disabled = false; + await el.updateComplete; + // Should be invalid while enabled + expect(new FormData(form).get("name")).to.equal("blah") + }); + it('should receive the correct validation attributes ("states") when valid', async () => { const el = await fixture(html` `); @@ -557,5 +574,25 @@ describe('', () => { }); }); + it("Should be invalid if the pattern is invalid", async () => { + const el = await fixture(html` `); + + el.formControl.focus(); + await el.updateComplete; + expect(el.checkValidity()).to.be.false + + await aTimeout(10) + await sendKeys({ type: "1234" }) + await el.updateComplete + await aTimeout(10) + + // For some reason this is only required in Safari. + if (isSafari) { + el.setCustomValidity("") + } + + expect(el.checkValidity()).to.be.true + }) + runFormControlBaseTests('wa-input'); }); diff --git a/src/internal/tabbable.test.ts b/src/internal/tabbable.test.ts index 8466efde9..69e98a002 100644 --- a/src/internal/tabbable.test.ts +++ b/src/internal/tabbable.test.ts @@ -1,7 +1,7 @@ import { aTimeout, elementUpdated, expect, fixture } from '@open-wc/testing'; import { activeElements, getDeepestActiveElement } from './active-elements.js'; -import { clickOnElement } from './test.js'; +import { clickOnElement, isSafari } from './test.js'; import { html } from 'lit'; import { sendKeys } from '@web/test-runner-commands'; import type { WaDialog } from '../webawesome.js'; @@ -14,8 +14,7 @@ async function holdShiftKey(callback: () => Promise) { await sendKeys({ up: 'Shift' }); } -const tabKey = - navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('HeadlessChrome') ? 'Alt+Tab' : 'Tab'; +const tabKey = isSafari ? 'Alt+Tab' : 'Tab'; // Simple helper to turn the activeElements generator into an array function activeElementsArray() { diff --git a/src/internal/test.ts b/src/internal/test.ts index b85257c47..02484a260 100644 --- a/src/internal/test.ts +++ b/src/internal/test.ts @@ -1,5 +1,7 @@ import { sendMouse } from '@web/test-runner-commands'; +export const isSafari = navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('HeadlessChrome') + function determineMousePosition(el: Element, position: string, offsetX: number, offsetY: number) { const { x, y, width, height } = el.getBoundingClientRect(); const centerX = Math.floor(x + window.pageXOffset + width / 2); diff --git a/src/internal/webawesome-element.ts b/src/internal/webawesome-element.ts index 2b1483c9b..5d87ebe57 100644 --- a/src/internal/webawesome-element.ts +++ b/src/internal/webawesome-element.ts @@ -286,6 +286,12 @@ export class WebAwesomeFormAssociated this.setValue(this.value, this.value); } + if (changedProperties.has("disabled")) { + if (!this.disabled) { + this.removeAttribute("disabled") + } + } + this.runValidators() super.willUpdate(changedProperties); }