diff --git a/src/components/button/button.component.ts b/src/components/button/button.component.ts index 5d43ada2b..dc5d2a635 100644 --- a/src/components/button/button.component.ts +++ b/src/components/button/button.component.ts @@ -1,18 +1,17 @@ import { classMap } from 'lit/directives/class-map.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 { 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 WaIcon from '../icon/icon.component.js'; import WaSpinner from '../spinner/spinner.component.js'; -import WebAwesomeElement from '../../internal/webawesome-element.js'; import type { CSSResultGroup } from 'lit'; -import type { WebAwesomeFormControl } from '../../internal/webawesome-element.js'; +import { MirrorValidator } from '../../internal/validators/mirror-validator.js'; /** * @summary Buttons represent actions that are available to the user. @@ -53,16 +52,18 @@ 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. */ -export default class WaButton extends WebAwesomeElement implements WebAwesomeFormControl { +export default class WaButton extends WebAwesomeFormAssociated { static styles: CSSResultGroup = [componentStyles, styles]; static dependencies = { 'wa-icon': WaIcon, 'wa-spinner': WaSpinner }; - 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); @@ -82,7 +83,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; @@ -132,7 +133,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() form: null | string; /** Used to override the form owner's `action` attribute. */ @property({ attribute: 'formaction' }) formAction: string; @@ -150,30 +151,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'); @@ -185,18 +162,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() { @@ -209,10 +209,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) { + // 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. */ @@ -230,37 +233,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`; diff --git a/src/components/input/input.component.ts b/src/components/input/input.component.ts index 0ee0266ac..5f9d50f7e 100644 --- a/src/components/input/input.component.ts +++ b/src/components/input/input.component.ts @@ -301,7 +301,7 @@ export default class WaInput extends WebAwesomeFormAssociated { // 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.runValidators() + this.updateValidity() } /** Sets focus on the input. */ diff --git a/src/components/input/input.test.ts b/src/components/input/input.test.ts index c05227d36..fa33a16d7 100644 --- a/src/components/input/input.test.ts +++ b/src/components/input/input.test.ts @@ -364,7 +364,7 @@ describe('', () => { expect(form.reportValidity()).to.be.false; }); - it('should be valid when the input is empty, reportValidity() is called, and the form has novalidate', async () => { + it('should be invalid when the input is empty, reportValidity() is called, and the form has novalidate', async () => { const form = await fixture(html`
@@ -372,7 +372,10 @@ describe('', () => { `); - expect(form.reportValidity()).to.be.true; + // 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; }); it('should be invalid when a native input is empty and form.reportValidity() is called', async () => { diff --git a/src/internal/test/form-control-base-tests.ts b/src/internal/test/form-control-base-tests.ts index 13056f4e5..a9230454d 100644 --- a/src/internal/test/form-control-base-tests.ts +++ b/src/internal/test/form-control-base-tests.ts @@ -175,32 +175,32 @@ function runSpecialTests_slButtonOfTypeButton(createControl: CreateControlFn) { it('should make sure that calling `.checkValidity()` will still return `true` when custom error has been set', async () => { const control = await createControl(); control.setCustomValidity('error'); - expect(control.checkValidity()).to.equal(true); + expect(control.checkValidity()).to.equal(false); }); it('should make sure that calling `.reportValidity()` will still return `true` when custom error has been set', async () => { const control = await createControl(); control.setCustomValidity('error'); - expect(control.reportValidity()).to.equal(true); + expect(control.reportValidity()).to.equal(false); }); - it('should not emit an `wa-invalid` event when `.checkValidity()` is called in custom error case, and not disabled', async () => { + it('should emit an `wa-invalid` event when `.checkValidity()` is called in custom error case, and not disabled', async () => { const control = await createControl(); control.setCustomValidity('error'); control.disabled = false; await control.updateComplete; const emittedEvents = checkEventEmissions(control, 'wa-invalid', () => control.checkValidity()); - expect(emittedEvents.length).to.equal(0); + expect(emittedEvents.length).to.equal(1); }); - it('should not emit an `wa-invalid` event when `.reportValidity()` is called in custom error case, and not disabled', async () => { + it('should emit an `wa-invalid` event when `.reportValidity()` is called in custom error case, and not disabled', async () => { const control = await createControl(); control.setCustomValidity('error'); control.disabled = false; await control.updateComplete; const emittedEvents = checkEventEmissions(control, 'wa-invalid', () => control.reportValidity()); - expect(emittedEvents.length).to.equal(0); + expect(emittedEvents.length).to.equal(1); }); } @@ -208,16 +208,16 @@ function runSpecialTests_slButtonOfTypeButton(createControl: CreateControlFn) { // Special tests for // function runSpecialTests_slButtonWithHref(createControl: CreateControlFn) { - it('should make sure that calling `.checkValidity()` will return `true` in custom error case', async () => { + it('should make sure that calling `.checkValidity()` will return `false` in custom error case', async () => { const control = await createControl(); control.setCustomValidity('error'); - expect(control.checkValidity()).to.equal(true); + expect(control.checkValidity()).to.equal(false); }); - it('should make sure that calling `.reportValidity()` will return `true` in custom error case', async () => { + it('should make sure that calling `.reportValidity()` will return `false` in custom error case', async () => { const control = await createControl(); control.setCustomValidity('error'); - expect(control.reportValidity()).to.equal(true); + expect(control.reportValidity()).to.equal(false); }); it('should not emit an `wa-invalid` event when `.checkValidity()` is called in custom error case', async () => { diff --git a/src/internal/webawesome-element.ts b/src/internal/webawesome-element.ts index 5d87ebe57..38e74101c 100644 --- a/src/internal/webawesome-element.ts +++ b/src/internal/webawesome-element.ts @@ -259,18 +259,24 @@ export class WebAwesomeFormAssociated console.warn('For further reading: https://github.com/whatwg/html/issues/8365'); } - this.addEventListener('invalid', this.emitInvalid); } connectedCallback() { super.connectedCallback(); - // Lazily evaluate after the constructor. + this.addEventListener('invalid', this.emitInvalid); + + // Lazily evaluate after the constructor to allow people to override the `assumeInteractionOn` this.assumeInteractionOn.forEach(event => { this.addEventListener(event, this.handleInteraction); }); } + firstUpdated (...args: Parameters) { + super.firstUpdated(...args) + this.updateValidity() + } + emitInvalid = (e: Event) => { if (e.target !== this) return; @@ -291,7 +297,7 @@ export class WebAwesomeFormAssociated this.removeAttribute("disabled") } } - this.runValidators() + this.updateValidity() super.willUpdate(changedProperties); } @@ -329,12 +335,12 @@ export class WebAwesomeFormAssociated } checkValidity() { - this.runValidators(); + this.updateValidity(); return this.internals.checkValidity(); } reportValidity() { - this.runValidators(); + this.updateValidity(); return this.internals.reportValidity(); } @@ -387,7 +393,7 @@ export class WebAwesomeFormAssociated this.hasInteracted = false; this.valueHasChanged = false; this.emittedEvents = []; - this.runValidators(); + this.updateValidity(); this.setValue(this.defaultValue, this.defaultValue); } @@ -426,7 +432,9 @@ export class WebAwesomeFormAssociated return [...staticValidators, ...validators]; } - runValidators() { + updateValidity() { + const parentForm = this.getForm() + if (this.disabled || this.getAttribute('disabled')) { this.setValidity({}); // We don't run validators on disabled thiss to be inline with native HTMLElements.