diff --git a/src/components/button/button.test.ts b/src/components/button/button.test.ts index efc3b68f8..ec09f93bb 100644 --- a/src/components/button/button.test.ts +++ b/src/components/button/button.test.ts @@ -206,43 +206,44 @@ describe('', () => { expect(submitter.formNoValidate).to.be.true; }); - it("should only submit button name / value pair when the form is submitted", async () => { - const form = await fixture(html`
- Button 1 - Button 2 -
`); + it('should only submit button name / value pair when the form is submitted', async () => { + const form = await fixture( + html`
+ Button 1 + Button 2 +
` + ); - let formData = new FormData(form) - let submitter: null | HTMLButtonElement = document.createElement("button") + let formData = new FormData(form); + let submitter: null | HTMLButtonElement = document.createElement('button'); + form.addEventListener('submit', e => { + e.preventDefault(); + formData = new FormData(form); + submitter = e.submitter as HTMLButtonElement; + }); - form.addEventListener("submit", (e) => { - e.preventDefault() - formData = new FormData(form) - submitter = e.submitter as HTMLButtonElement - }) + expect(formData.get('btn-1')).to.be.null; + expect(formData.get('btn-2')).to.be.null; - expect(formData.get("btn-1")).to.be.null - expect(formData.get("btn-2")).to.be.null + form.querySelector('wa-button')?.click(); + await aTimeout(0); - form.querySelector("wa-button")?.click() - await aTimeout(0) + expect(formData.get('btn-1')).to.be.null; + expect(formData.get('btn-2')).to.be.null; - expect(formData.get("btn-1")).to.be.null - expect(formData.get("btn-2")).to.be.null + expect(submitter.name).to.equal('btn-1'); + expect(submitter.value).to.equal('value-1'); - expect(submitter.name).to.equal("btn-1") - expect(submitter.value).to.equal("value-1") + form.querySelectorAll('wa-button')[1]?.click(); + await aTimeout(0); - form.querySelectorAll("wa-button")[1]?.click() - await aTimeout(0) + expect(formData.get('btn-1')).to.be.null; + expect(formData.get('btn-2')).to.be.null; - expect(formData.get("btn-1")).to.be.null - expect(formData.get("btn-2")).to.be.null - - expect(submitter.name).to.equal("btn-2") - expect(submitter.value).to.equal("value-2") - }) + expect(submitter.name).to.equal('btn-2'); + expect(submitter.value).to.equal('value-2'); + }); }); describe('when using methods', () => { diff --git a/src/components/checkbox/checkbox.test.ts b/src/components/checkbox/checkbox.test.ts index 0a70eab88..863da03dd 100644 --- a/src/components/checkbox/checkbox.test.ts +++ b/src/components/checkbox/checkbox.test.ts @@ -188,7 +188,7 @@ describe('', () => { it('should be valid when required and checked', async () => { const checkbox = await fixture(html` `); - await checkbox.updateComplete + await checkbox.updateComplete; expect(checkbox.checkValidity()).to.be.true; }); diff --git a/src/components/checkbox/checkbox.ts b/src/components/checkbox/checkbox.ts index 75c1de622..edf3e9e0e 100644 --- a/src/components/checkbox/checkbox.ts +++ b/src/components/checkbox/checkbox.ts @@ -48,13 +48,11 @@ import type { CSSResultGroup } from 'lit'; * @cssproperty --box-shadow - The shadow effects around the edges of the checkbox. * @cssproperty --toggle-size - The size of the checkbox. */ -@customElement("wa-checkbox") +@customElement('wa-checkbox') export default class WaCheckbox extends WebAwesomeFormAssociated { static styles: CSSResultGroup = [componentStyles, styles]; - static get validators () { - return [ - GroupRequiredValidator(), - ] + static get validators() { + return [GroupRequiredValidator()]; } private readonly hasSlotController = new HasSlotController(this, 'help-text'); @@ -88,7 +86,7 @@ export default class WaCheckbox extends WebAwesomeFormAssociated { @property({ type: Boolean, reflect: true }) indeterminate = false; /** The default value of the form control. Primarily used for resetting the form control. */ - @property({ type: Boolean, reflect: true, attribute: "checked" }) defaultChecked = false; + @property({ type: Boolean, reflect: true, attribute: 'checked' }) defaultChecked = false; /** * By default, form controls are associated with the nearest containing `
` element. This attribute allows you @@ -123,24 +121,24 @@ export default class WaCheckbox extends WebAwesomeFormAssociated { this.emit('wa-focus'); } - @watch(["defaultChecked"]) - handleDefaultCheckedChange () { + @watch(['defaultChecked']) + handleDefaultCheckedChange() { if (!this.hasInteracted && this.checked !== this.defaultChecked) { - this.checked = this.defaultChecked - this.value = this.checked ? this.value || 'on' : null + this.checked = this.defaultChecked; + this.value = this.checked ? this.value || 'on' : null; // These @watch() commands seem to override the base element checks for changes, so we need to setValue for the form and and updateValidity() this.setValue(this.value, this.value); - this.updateValidity() + this.updateValidity(); } } - @watch(["value", "checked"], { waitUntilFirstUpdate: true }) - handleValueOrCheckedChange () { - this.value = this.checked ? this.value || 'on' : null + @watch(['value', 'checked'], { waitUntilFirstUpdate: true }) + handleValueOrCheckedChange() { + this.value = this.checked ? this.value || 'on' : null; // These @watch() commands seem to override the base element checks for changes, so we need to setValue for the form and and updateValidity() this.setValue(this.value, this.value); - this.updateValidity() + this.updateValidity(); } @watch(['checked', 'indeterminate'], { waitUntilFirstUpdate: true }) @@ -150,13 +148,13 @@ export default class WaCheckbox extends WebAwesomeFormAssociated { this.updateValidity(); } - formResetCallback () { + formResetCallback() { // Evaluate checked before the super call because of our watcher on value. - super.formResetCallback() - this.checked = this.defaultChecked - this.value = this.checked ? this.value || 'on' : null + super.formResetCallback(); + this.checked = this.defaultChecked; + this.value = this.checked ? this.value || 'on' : null; this.setValue(this.value, this.value); - this.updateValidity() + this.updateValidity(); } /** Simulates a click on the checkbox. */ diff --git a/src/components/color-picker/color-picker.ts b/src/components/color-picker/color-picker.ts index 95fbb2c89..36247515d 100644 --- a/src/components/color-picker/color-picker.ts +++ b/src/components/color-picker/color-picker.ts @@ -91,14 +91,12 @@ declare const EyeDropper: EyeDropperConstructor; * @cssproperty --slider-handle-size - The diameter of the slider's handle. * @cssproperty --swatch-size - The size of each predefined color swatch. */ -@customElement("wa-color-picker") +@customElement('wa-color-picker') export default class WaColorPicker extends WebAwesomeFormAssociated { static styles: CSSResultGroup = [componentStyles, styles]; - static get validators () { - return [ - RequiredValidator(), - ] + static get validators() { + return [RequiredValidator()]; } private isSafeValue = false; @@ -109,15 +107,15 @@ export default class WaColorPicker extends WebAwesomeFormAssociated { // @TODO: This is a hacky way to show the "Please fill out this field", do we want the old behavior where it opens the dropdown? // or is the new behavior okay? - get validationTarget () { + get validationTarget() { // This puts the popup on the element only if the color picker is expanded. if (!this.inline && this.dropdown?.open) { - return this.input + return this.input; } // This puts popup on the colorpicker itself without needing to expand it to show the input. // This is necessary because form submissions expect the "anchor" to be currently shown. - return this.trigger + return this.trigger; } @query('.color-dropdown') dropdown: WaDropdown; @@ -138,10 +136,10 @@ export default class WaColorPicker extends WebAwesomeFormAssociated { * in a specific format, use the `getFormattedValue()` method. The value is submitted as a name/value pair with form * data. */ - @property({attribute: false}) value = ''; + @property({ attribute: false }) value = ''; /** The default value of the form control. Primarily used for resetting the form control. */ - @property({attribute: "value", reflect: true}) defaultValue = ''; + @property({ attribute: 'value', reflect: true }) defaultValue = ''; /** * The color picker's label. This will not be displayed, but it will be announced by assistive devices. If you need to @@ -620,12 +618,12 @@ export default class WaColorPicker extends WebAwesomeFormAssociated { private handleAfterHide() { this.previewButton.classList.remove('color-picker__preview-color--copied'); // Update validity so we get a new anchor. - this.updateValidity() + this.updateValidity(); } private handleAfterShow() { // Update validity so we get a new anchor. - this.updateValidity() + this.updateValidity(); } private handleEyeDropper() { @@ -694,7 +692,6 @@ export default class WaColorPicker extends WebAwesomeFormAssociated { handleValueChange(oldValue: string | undefined, newValue: string) { this.isEmpty = !newValue; - if (!newValue) { this.hue = 0; this.saturation = 0; @@ -787,13 +784,29 @@ 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") + this.emit('wa-invalid'); } return false; } - return super.reportValidity() + return super.reportValidity(); + } + + formStateRestoreCallback(...args: Parameters) { + 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(); } render() { diff --git a/src/components/input/input.test.ts b/src/components/input/input.test.ts index fa33a16d7..7ce25ffc1 100644 --- a/src/components/input/input.test.ts +++ b/src/components/input/input.test.ts @@ -1,11 +1,11 @@ // eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment 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 { isSafari } from '../../internal/test.js'; +import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests.js'; // must come from the same module +import { sendKeys } from '@web/test-runner-commands'; import sinon from 'sinon'; import type WaInput from './input.js'; -import { isSafari } from '../../internal/test.js'; describe('', () => { it('should pass accessibility tests', async () => { @@ -168,16 +168,18 @@ describe('', () => { }); 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" + 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) + 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") + expect(new FormData(form).get('name')).to.equal('blah'); }); it('should receive the correct validation attributes ("states") when valid', async () => { @@ -577,25 +579,25 @@ describe('', () => { }); }); - it("Should be invalid if the pattern is invalid", async () => { + 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 + expect(el.checkValidity()).to.be.false; - await aTimeout(10) - await sendKeys({ type: "1234" }) - await el.updateComplete - await aTimeout(10) + 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("") + el.setCustomValidity(''); } - expect(el.checkValidity()).to.be.true - }) + expect(el.checkValidity()).to.be.true; + }); runFormControlBaseTests('wa-input'); }); diff --git a/src/components/input/input.ts b/src/components/input/input.ts index 7021b4430..fa7178e7b 100644 --- a/src/components/input/input.ts +++ b/src/components/input/input.ts @@ -55,17 +55,15 @@ import type { CSSResultGroup } from 'lit'; * @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. */ -@customElement("wa-input") +@customElement('wa-input') export default class WaInput extends WebAwesomeFormAssociated { static styles: CSSResultGroup = [componentStyles, formControlStyles, styles]; - static get validators () { - return [ - MirrorValidator() - ] + static get validators() { + return [MirrorValidator()]; } - assumeInteractionOn = ['wa-blur', 'wa-input'] + assumeInteractionOn = ['wa-blur', 'wa-input']; private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label'); private readonly localize = new LocalizeController(this); @@ -97,10 +95,10 @@ export default class WaInput extends WebAwesomeFormAssociated { @property() name = ''; /** The current value of the input, submitted as a name/value pair with form data. */ - @property({attribute: false}) value = ''; + @property({ attribute: false }) value = ''; /** The default value of the form control. Primarily used for resetting the form control. */ - @property({attribute: "value", reflect: true}) defaultValue = ''; + @property({ attribute: 'value', reflect: true }) defaultValue = ''; /** The input's size. */ @property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium'; @@ -143,7 +141,7 @@ export default class WaInput extends WebAwesomeFormAssociated { * 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 = null + @property({ reflect: true }) form = null; /** Makes the input a required field. */ @property({ type: Boolean, reflect: true }) required = false; @@ -279,7 +277,7 @@ export default class WaInput extends WebAwesomeFormAssociated { // See https://github.com/shoelace-style/shoelace/pull/988 // if (!event.defaultPrevented && !event.isComposing) { - this.getForm()?.requestSubmit(null) + this.getForm()?.requestSubmit(null); } }); } @@ -294,7 +292,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.updateValidity() + this.updateValidity(); } /** Sets focus on the input. */ @@ -361,19 +359,19 @@ export default class WaInput extends WebAwesomeFormAssociated { } } - formStateRestoreCallback (...args: Parameters) { - const [value, reason] = args - super.formStateRestoreCallback(value, reason) + formStateRestoreCallback(...args: Parameters) { + 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 + this.input.value = value; } - formResetCallback () { + formResetCallback() { this.input.value = this.defaultValue; this.value = this.defaultValue; - super.formResetCallback() + super.formResetCallback(); } render() { diff --git a/src/components/radio-button/radio-button.ts b/src/components/radio-button/radio-button.ts index c4682f538..70ae46467 100644 --- a/src/components/radio-button/radio-button.ts +++ b/src/components/radio-button/radio-button.ts @@ -1,9 +1,9 @@ import { classMap } from 'lit/directives/class-map.js'; +import { customElement, property, query, state } from 'lit/decorators.js'; import { HasSlotController } from '../../internal/slot.js'; import { html } from 'lit/static-html.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { LitElement } from 'lit'; -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'; @@ -30,7 +30,7 @@ import type { CSSResultGroup } from 'lit'; * @csspart label - The container that wraps the radio button's label. * @csspart suffix - The container that wraps the suffix. */ -@customElement("wa-radio-button") +@customElement('wa-radio-button') export default class WaRadioButton extends WebAwesomeFormAssociated { static styles: CSSResultGroup = [componentStyles, styles]; @@ -46,7 +46,7 @@ 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; + @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; @@ -66,10 +66,10 @@ export default class WaRadioButton extends WebAwesomeFormAssociated { /** * The string pointing to a form's id. */ - @property({ reflect: true }) form: string | null = null + @property({ reflect: true }) form: string | null = null; /** Needed for Form Validation. Without it we get a console error. */ - static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true } + static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }; connectedCallback() { super.connectedCallback(); @@ -151,7 +151,6 @@ export default class WaRadioButton extends WebAwesomeFormAssociated { } } - declare global { interface HTMLElementTagNameMap { 'wa-radio-button': WaRadioButton; diff --git a/src/components/radio-group/radio-group.test.ts b/src/components/radio-group/radio-group.test.ts index 5f32b6c32..ce3e56db9 100644 --- a/src/components/radio-group/radio-group.test.ts +++ b/src/components/radio-group/radio-group.test.ts @@ -1,4 +1,4 @@ -import { aTimeout, expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing'; +import { aTimeout, expect, fixture, html, oneEvent } from '@open-wc/testing'; import { clickOnElement } from '../../internal/test.js'; import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests.js'; import { sendKeys } from '@web/test-runner-commands'; @@ -218,9 +218,9 @@ describe('when submitting a form', () => { const radio = form.querySelectorAll('wa-radio')[1]; radio.click(); - await form.querySelector("wa-radio-group")?.updateComplete + await form.querySelector('wa-radio-group')?.updateComplete; - const formData = new FormData(form) + const formData = new FormData(form); expect(formData.get('a')).to.equal('2'); }); diff --git a/src/components/radio-group/radio-group.ts b/src/components/radio-group/radio-group.ts index aeb72c10e..5eb9538b6 100644 --- a/src/components/radio-group/radio-group.ts +++ b/src/components/radio-group/radio-group.ts @@ -1,9 +1,9 @@ import '../button-group/button-group.js'; import '../radio/radio.js'; import { classMap } from 'lit/directives/class-map.js'; +import { customElement, property, query, state } from 'lit/decorators.js'; import { HasSlotController } from '../../internal/slot.js'; import { html, LitElement } from 'lit'; -import { property, query, state } from 'lit/decorators.js'; import { RequiredValidator } from '../../internal/validators/required-validator.js'; import { watch } from '../../internal/watch.js'; import { WebAwesomeFormAssociated } from '../../internal/webawesome-element.js'; @@ -38,13 +38,12 @@ import type WaRadioButton from '../radio-button/radio-button.js'; * @csspart button-group - The button group that wraps radio buttons. * @csspart button-group__base - The button group's `base` part. */ +@customElement('wa-radio-group') export default class WaRadioGroup extends WebAwesomeFormAssociated { static styles: CSSResultGroup = [componentStyles, formControlStyles, styles]; - static get validators () { - return [ - RequiredValidator() - ] + static get validators() { + return [RequiredValidator()]; } private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label'); @@ -66,7 +65,7 @@ export default class WaRadioGroup extends WebAwesomeFormAssociated { @property({ reflect: true }) name = null; @property({ attribute: false }) value: string | null = null; - @property({ attribute: "value", reflect: true }) defaultValue: string | null = null; + @property({ attribute: 'value', reflect: true }) defaultValue: string | null = null; /** The radio group's size. This size will be applied to all child radios and radio buttons. */ @property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium'; @@ -78,29 +77,33 @@ export default class WaRadioGroup extends WebAwesomeFormAssociated { * We need this because if we don't have it, FormValidation yells at us that it's "not focusable". * If we use `this.tabIndex = -1` we can't focus the radio inside. */ - static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true } + static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }; - constructor () { - super() + constructor() { + super(); - this.addEventListener("keydown", this.handleKeyDown) + this.addEventListener('keydown', this.handleKeyDown); this.addEventListener('click', this.handleRadioClick); } private handleRadioClick = (e: Event) => { - const clickedRadio = (e.target as HTMLElement).closest("wa-radio, wa-radio-button") + const clickedRadio = (e.target as HTMLElement).closest('wa-radio, wa-radio-button'); - if (!clickedRadio) return - if (clickedRadio.disabled) { return } + if (!clickedRadio) return; + if (clickedRadio.disabled) { + return; + } - const oldValue = this.value - this.value = clickedRadio.value + const oldValue = this.value; + this.value = clickedRadio.value; clickedRadio.checked = true; - const radios = this.getAllRadios() + const radios = this.getAllRadios(); const hasButtonGroup = radios.some(radio => radio.tagName.toLowerCase() === 'wa-radio-button'); for (const radio of radios) { - if (clickedRadio === radio) { continue } + if (clickedRadio === radio) { + continue; + } radio.checked = false; @@ -115,7 +118,6 @@ export default class WaRadioGroup extends WebAwesomeFormAssociated { } }; - private getAllRadios() { return [...this.querySelectorAll('wa-radio, wa-radio-button')]; } @@ -141,9 +143,9 @@ export default class WaRadioGroup extends WebAwesomeFormAssociated { radio.size = this.size; if (!radio.disabled && radio.value === this.value) { - radio.checked = true + radio.checked = true; } else { - radio.checked = false + radio.checked = false; } }) ); @@ -195,13 +197,13 @@ export default class WaRadioGroup extends WebAwesomeFormAssociated { * We use the first available radio as the validationTarget similar to native HTML that shows the validation popup on * the first radio element. */ - get validationTarget () { + get validationTarget() { return this.querySelector(':is(wa-radio, wa-radio-button):not([disabled])') || undefined; } - @watch("value") - handleValueChange () { - this.syncRadioElements() + @watch('value') + handleValueChange() { + this.syncRadioElements(); } @watch('size', { waitUntilFirstUpdate: true }) @@ -209,12 +211,12 @@ export default class WaRadioGroup extends WebAwesomeFormAssociated { this.syncRadios(); } - formResetCallback (...args: Parameters) { - this.value = this.defaultValue + formResetCallback(...args: Parameters) { + this.value = this.defaultValue; - super.formResetCallback(...args) + super.formResetCallback(...args); - this.syncRadioElements() + this.syncRadioElements(); } private handleKeyDown(event: KeyboardEvent) { @@ -222,19 +224,21 @@ export default class WaRadioGroup extends WebAwesomeFormAssociated { return; } - event.preventDefault() + event.preventDefault(); const radios = this.getAllRadios().filter(radio => !radio.disabled); - if (radios.length <= 0) { return } + if (radios.length <= 0) { + return; + } - const oldValue = this.value + const oldValue = this.value; const checkedRadio = radios.find(radio => radio.checked) ?? radios[0]; const incr = event.key === ' ' ? 0 : ['ArrowUp', 'ArrowLeft'].includes(event.key) ? -1 : 1; let index = radios.indexOf(checkedRadio) + incr; - if (!index) index = 0 + if (!index) index = 0; if (index < 0) { index = radios.length - 1; @@ -254,7 +258,7 @@ export default class WaRadioGroup extends WebAwesomeFormAssociated { } }); - this.value = radios[index].value + this.value = radios[index].value; radios[index].checked = true; if (!hasButtonGroup) { @@ -277,9 +281,7 @@ export default class WaRadioGroup extends WebAwesomeFormAssociated { const hasHelpTextSlot = this.hasSlotController.test('help-text'); const hasLabel = this.label ? true : !!hasLabelSlot; const hasHelpText = this.helpText ? true : !!hasHelpTextSlot; - const defaultSlot = html` - - `; + const defaultSlot = html` `; return html`
', () => { `); - expect(el.value).to.equal("option-1") - expect(el.defaultValue).to.equal("option-1") - expect(el.displayInput.value).to.equal("Option 1") + expect(el.value).to.equal('option-1'); + expect(el.defaultValue).to.equal('option-1'); + expect(el.displayInput.value).to.equal('Option 1'); const secondOption = el.querySelectorAll('wa-option')[1]; const changeHandler = sinon.spy(); diff --git a/src/components/select/select.ts b/src/components/select/select.ts index 0d7355ed0..dc3752638 100644 --- a/src/components/select/select.ts +++ b/src/components/select/select.ts @@ -3,11 +3,11 @@ import '../popup/popup.js'; import '../tag/tag.js'; import { animateTo, stopAnimations } from '../../internal/animate.js'; import { classMap } from 'lit/directives/class-map.js'; +import { customElement, property, query, state } from 'lit/decorators.js'; import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry.js'; import { HasSlotController } from '../../internal/slot.js'; import { html } from 'lit'; import { LocalizeController } from '../../utilities/localize.js'; -import { customElement, property, query, state } from 'lit/decorators.js'; import { RequiredValidator } from '../../internal/validators/required-validator.js'; import { scrollIntoView } from '../../internal/scroll.js'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; @@ -74,16 +74,14 @@ import type WaPopup from '../popup/popup.js'; * @cssproperty --border-width - The width of the select's borders, including the listbox. * @cssproperty --box-shadow - The shadow effects around the edges of the select's combobox. */ -@customElement("wa-select") +@customElement('wa-select') export default class WaSelect extends WebAwesomeFormAssociated { static styles: CSSResultGroup = [componentStyles, formControlStyles, styles]; - assumeInteractionOn =['wa-blur', 'wa-input'] + assumeInteractionOn = ['wa-blur', 'wa-input']; - static get validators () { - return [ - RequiredValidator() - ] + static get validators() { + return [RequiredValidator()]; } private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label'); @@ -99,8 +97,8 @@ export default class WaSelect extends WebAwesomeFormAssociated { @query('.select__listbox') listbox: HTMLSlotElement; /** Where to anchor native constraint validation */ - get validationTarget () { - return this.valueInput + get validationTarget() { + return this.valueInput; } @state() private hasFocus = false; @@ -122,29 +120,31 @@ export default class WaSelect extends WebAwesomeFormAssociated { private _defaultValue: string | string[] = ''; @property({ - attribute: "value", + attribute: 'value', reflect: true, converter: { - fromAttribute: (value: string) => value.split(" "), - toAttribute: (value: string | string[]) => Array.isArray(value) ? value.join(' ') : value + fromAttribute: (value: string) => value.split(' '), + toAttribute: (value: string | string[]) => (Array.isArray(value) ? value.join(' ') : value) } }) // @ts-expect-error defaultValue () is a property on the host, but is being used a getter / setter here. set defaultValue(val: string | string[]) { // For some reason this can go off before we've fully updated. So check the attribute too. - const isMultiple = this.multiple || this.hasAttribute("multiple") + const isMultiple = this.multiple || this.hasAttribute('multiple'); if (!isMultiple && Array.isArray(val)) { - val = val.join(" ") + val = val.join(' '); } - this._defaultValue = val + this._defaultValue = val; - if (!this.hasInteracted) { - this.value = this.defaultValue + if (!this.hasInteracted) { + this.value = this.defaultValue; } } - get defaultValue() { return this._defaultValue; } + get defaultValue() { + return this._defaultValue; + } /** The select's size. */ @property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium'; @@ -235,12 +235,11 @@ export default class WaSelect extends WebAwesomeFormAssociated { connectedCallback() { super.connectedCallback(); - this.updateComplete.then(() => { - if (!this.hasInteracted) { - this.value = this.defaultValue + if (!this.hasInteracted) { + this.value = this.defaultValue; } - }) + }); // Because this is a form control, it shouldn't be opened initially this.open = false; } @@ -627,7 +626,7 @@ export default class WaSelect extends WebAwesomeFormAssociated { // Update validity this.updateComplete.then(() => { - this.updateValidity() + this.updateValidity(); }); } protected get tags() { @@ -643,7 +642,7 @@ export default class WaSelect extends WebAwesomeFormAssociated { return html`+${this.selectedOptions.length - index}`; } return html``; - }) + }); } @watch('disabled', { waitUntilFirstUpdate: true }) @@ -662,7 +661,7 @@ export default class WaSelect extends WebAwesomeFormAssociated { // Select only the options that match the new value this.setSelectedOptions(allOptions.filter(el => value.includes(el.value))); - this.updateValidity() + this.updateValidity(); } @watch('open', { waitUntilFirstUpdate: true }) @@ -740,10 +739,10 @@ export default class WaSelect extends WebAwesomeFormAssociated { this.displayInput.blur(); } - formResetCallback () { + formResetCallback() { this.value = this.defaultValue; - super.formResetCallback() - this.handleValueChange() + super.formResetCallback(); + this.handleValueChange(); } render() { diff --git a/src/components/textarea/textarea.ts b/src/components/textarea/textarea.ts index d22b23998..1c2c6e878 100644 --- a/src/components/textarea/textarea.ts +++ b/src/components/textarea/textarea.ts @@ -1,10 +1,10 @@ import { classMap } from 'lit/directives/class-map.js'; +import { customElement, property, query, state } from 'lit/decorators.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 { 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'; @@ -41,14 +41,12 @@ import type { CSSResultGroup } from 'lit'; * @cssproperty --border-width - The width of the textarea's borders. * @cssproperty --box-shadow - The shadow effects around the edges of the textarea. */ -@customElement("wa-textarea") +@customElement('wa-textarea') export default class WaTextarea extends WebAwesomeFormAssociated { static formAssociated = true; static styles: CSSResultGroup = [componentStyles, formControlStyles, styles]; static get validators() { - return [ - MirrorValidator() - ]; + return [MirrorValidator()]; } assumeInteractionOn = ['wa-blur', 'wa-input']; @@ -146,12 +144,12 @@ export default class WaTextarea extends WebAwesomeFormAssociated { @property() inputmode: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url'; /** The default value of the form control. Primarily used for resetting the form control. */ - @property({ reflect: true, attribute: "value" }) defaultValue: string = '' + @property({ reflect: true, attribute: 'value' }) defaultValue: string = ''; connectedCallback() { super.connectedCallback(); - this.value = this.defaultValue + this.value = this.defaultValue; this.resizeObserver = new ResizeObserver(() => this.setTextareaHeight()); this.updateComplete.then(() => { @@ -266,19 +264,19 @@ export default class WaTextarea extends WebAwesomeFormAssociated { } } - formStateRestoreCallback (...args: Parameters) { - const [value, reason] = args - super.formStateRestoreCallback(value, reason) + formStateRestoreCallback(...args: Parameters) { + 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 + this.input.value = value; } - formResetCallback () { + formResetCallback() { this.input.value = this.defaultValue; this.value = this.defaultValue; - super.formResetCallback() + super.formResetCallback(); } render() { diff --git a/src/internal/form.ts b/src/internal/form.ts index 02f44f16d..6e541ec8b 100644 --- a/src/internal/form.ts +++ b/src/internal/form.ts @@ -83,7 +83,7 @@ export class FormControlController implements ReactiveController { return input.closest('form'); }, - name: input => input.name || "", + name: input => input.name || '', value: input => input.value, defaultValue: input => input.defaultValue, disabled: input => input.disabled ?? false, diff --git a/src/internal/test.ts b/src/internal/test.ts index 02484a260..cd186cb38 100644 --- a/src/internal/test.ts +++ b/src/internal/test.ts @@ -1,6 +1,6 @@ import { sendMouse } from '@web/test-runner-commands'; -export const isSafari = navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('HeadlessChrome') +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(); diff --git a/src/internal/validators/group-required-validator.ts b/src/internal/validators/group-required-validator.ts index 14be29643..e79ea725b 100644 --- a/src/internal/validators/group-required-validator.ts +++ b/src/internal/validators/group-required-validator.ts @@ -4,74 +4,73 @@ import type { Validator } from '../webawesome-element.js'; // https://codepen.io/paramagicdev/pen/eYorwrz export const GroupRequiredValidator = (): Validator => { const obj: Validator = { - observedAttributes: ["required"], - message (element) { - const tagName = element.tagName.toLowerCase() - if (tagName === "wa-checkbox") { - return "Please check this box if you want to proceed" // @TODO: Add a translation. + observedAttributes: ['required'], + message(element) { + const tagName = element.tagName.toLowerCase(); + if (tagName === 'wa-checkbox') { + return 'Please check this box if you want to proceed'; // @TODO: Add a translation. } - if (tagName === "wa-radio") { - return "Please select one of these options" // @TODO: Add a translation. + if (tagName === 'wa-radio') { + return 'Please select one of these options'; // @TODO: Add a translation. } - return "Please provide select a value for this group" // Not sure what to do here? + return 'Please provide select a value for this group'; // Not sure what to do here? }, - checkValidity (this: typeof GroupRequiredValidator, element) { - const validity: ReturnType = { - message: "", + checkValidity(this: typeof GroupRequiredValidator, element) { + const validity: ReturnType = { + message: '', isValid: true, invalidKeys: [] - } + }; const markInvalid = () => { - validity.message = typeof obj.message === "function" ? obj.message(element) : (obj.message || "") - validity.isValid = false - validity.invalidKeys.push("valueMissing") - } + validity.message = typeof obj.message === 'function' ? obj.message(element) : obj.message || ''; + validity.isValid = false; + validity.invalidKeys.push('valueMissing'); + }; - const isRequired = element.required ?? element.hasAttribute("required") + const isRequired = element.required ?? element.hasAttribute('required'); // Always valid if the element isn't required. // Always valid if no name. if (!isRequired) { - return validity + return validity; } // If there's no name, we just check if the individual element has a value. - if (!element.name || !element.getAttribute("name")) { - const value = element.value + if (!element.name || !element.getAttribute('name')) { + const value = element.value; - let isEmpty = !value + let isEmpty = !value; if (Array.isArray(value)) { - isEmpty = value.length === 0 + isEmpty = value.length === 0; } if (isEmpty) { - markInvalid() + markInvalid(); } - return validity + return validity; } - const form = element.getForm() + const form = element.getForm(); // Can't evaluate if there is no form. if (!form) { - return validity + return validity; } - - const formDataValue = new FormData(form).get(element.name) + const formDataValue = new FormData(form).get(element.name); // Can't do !formDataValue because we don't want "false" to trigger. False could technically be valid. - if (formDataValue === null || formDataValue === undefined || formDataValue === "") { - markInvalid() + if (formDataValue === null || formDataValue === undefined || formDataValue === '') { + markInvalid(); } - return validity + return validity; } - } + }; - return obj -} + return obj; +}; diff --git a/src/internal/validators/mirror-validator.ts b/src/internal/validators/mirror-validator.ts index 8a260b958..8555a5526 100644 --- a/src/internal/validators/mirror-validator.ts +++ b/src/internal/validators/mirror-validator.ts @@ -16,12 +16,12 @@ export const MirrorValidator = (): Validator => { }; if (!formControl) { - return validity + return validity; } - let isValid = true + let isValid = true; - if ("checkValidity" in formControl) { + if ('checkValidity' in formControl) { isValid = formControl.checkValidity(); } @@ -31,15 +31,14 @@ export const MirrorValidator = (): Validator => { validity.isValid = false; - if ("validationMessage" in formControl) { + if ('validationMessage' in formControl) { validity.message = formControl.validationMessage; } - // For some reason formControl doesn't have "validity", so chalk it up to customError - if (!("validity" in formControl)) { - validity.invalidKeys.push("customError"); - return validity + if (!('validity' in formControl)) { + validity.invalidKeys.push('customError'); + return validity; } for (const key in formControl.validity) { @@ -56,5 +55,5 @@ export const MirrorValidator = (): Validator => { return validity; } - } + }; }; diff --git a/src/internal/validators/required-validator.ts b/src/internal/validators/required-validator.ts index 0c4f5b299..f07b460b9 100644 --- a/src/internal/validators/required-validator.ts +++ b/src/internal/validators/required-validator.ts @@ -2,39 +2,39 @@ import type { Validator } from '../webawesome-element.js'; export const RequiredValidator = (): Validator => { const obj: Validator = { - observedAttributes: ["required"], - message: "Please fill out this field", // @TODO: Add a translation. - checkValidity (element) { - const validity: ReturnType = { - message: "", + observedAttributes: ['required'], + message: 'Please fill out this field', // @TODO: Add a translation. + checkValidity(element) { + const validity: ReturnType = { + message: '', isValid: true, invalidKeys: [] - } + }; - const isRequired = element.required ?? element.hasAttribute("required") + const isRequired = element.required ?? element.hasAttribute('required'); // Always true if the element isn't required. if (!isRequired) { - return validity + return validity; } - const value = element.value + const value = element.value; - let isEmpty = !value + let isEmpty = !value; if (Array.isArray(value)) { - isEmpty = value.length === 0 + isEmpty = value.length === 0; } if (isEmpty) { - validity.message = typeof obj.message === "function" ? obj.message(element) : (obj.message || "") - validity.isValid = false - validity.invalidKeys.push("valueMissing") + validity.message = typeof obj.message === 'function' ? obj.message(element) : obj.message || ''; + validity.isValid = false; + validity.invalidKeys.push('valueMissing'); } - return validity + return validity; } - } + }; - return obj -} + return obj; +}; diff --git a/src/internal/webawesome-element.ts b/src/internal/webawesome-element.ts index b2adb56f4..c8745dc44 100644 --- a/src/internal/webawesome-element.ts +++ b/src/internal/webawesome-element.ts @@ -94,16 +94,14 @@ export default class WebAwesomeElement extends LitElement { } } -export interface Validator< - T extends WebAwesomeFormAssociated = WebAwesomeFormAssociated -> { +export interface Validator { observedAttributes?: string[]; checkValidity: (element: T) => { message: string; isValid: boolean; invalidKeys: Exclude[]; }; - message?: (string | ((element: T) => string)); + message?: string | ((element: T) => string); } export interface WebAwesomeFormControl extends WebAwesomeElement { @@ -184,7 +182,7 @@ export class WebAwesomeFormAssociated assumeInteractionOn: string[] = ['wa-input']; // Additional - formControl?: HTMLElement & {value: unknown} | HTMLInputElement | HTMLTextAreaElement; + formControl?: (HTMLElement & { value: unknown }) | HTMLInputElement | HTMLTextAreaElement; validators: Validator[] = []; @@ -193,7 +191,7 @@ export class WebAwesomeFormAssociated @property({ state: true }) hasInteracted: boolean = false; // This works around a limitation in Safari. It is a hacky way for us to preserve customErrors generated by the user. - __manualCustomError = false + __manualCustomError = false; private emittedEvents: string[] = []; @@ -203,22 +201,21 @@ export class WebAwesomeFormAssociated try { this.internals = this.attachInternals(); } catch (_e) { - console.error('Element internals are not supported in your browser. Consider using a polyfill'); + // console.error('Element internals are not supported in your browser. Consider using a polyfill'); } const ctor = this.constructor as typeof LitElement; if (ctor.properties?.disabled?.reflect === true) { - console.warn(`The following element has their "disabled" property set to reflect.`); - console.warn(this); - console.warn('For further reading: https://github.com/whatwg/html/issues/8365'); + // console.warn(`The following element has their "disabled" property set to reflect.`); + // console.warn(this); + // console.warn('For further reading: https://github.com/whatwg/html/issues/8365'); } - } connectedCallback() { super.connectedCallback(); - this.updateValidity() + this.updateValidity(); // eslint-disable-next-line this.addEventListener('invalid', this.emitInvalid); @@ -229,9 +226,9 @@ export class WebAwesomeFormAssociated }); } - firstUpdated (...args: Parameters) { - super.firstUpdated(...args) - this.updateValidity() + firstUpdated(...args: Parameters) { + super.firstUpdated(...args); + this.updateValidity(); } emitInvalid = (e: Event) => { @@ -241,27 +238,25 @@ export class WebAwesomeFormAssociated }; protected willUpdate(changedProperties: PropertyValues) { - if (changedProperties.has("defaultValue")) { - if (!this.hasInteracted){ - this.value = this.defaultValue + if (changedProperties.has('defaultValue')) { + if (!this.hasInteracted) { + this.value = this.defaultValue; } } - if ( - changedProperties.has('value') - ) { + if (changedProperties.has('value')) { if (this.hasInteracted && this.value !== this.defaultValue) { - this.valueHasChanged = true + this.valueHasChanged = true; } - const value = this.value + const value = this.value; // Accounts for the snowflake case on `` if (Array.isArray(value)) { if (this.name) { - const formData = new FormData() + const formData = new FormData(); for (const val of value) { - formData.append(this.name, val as string) + formData.append(this.name, val as string); } this.setValue(formData, formData); } @@ -270,7 +265,7 @@ export class WebAwesomeFormAssociated } } - this.updateValidity() + this.updateValidity(); super.willUpdate(changedProperties); } @@ -335,10 +330,10 @@ export class WebAwesomeFormAssociated this.internals.setValidity(flags, message, anchor || undefined); - this.setCustomStates() + this.setCustomStates(); } - setCustomStates () { + setCustomStates() { const required = Boolean(this.required); const isValid = this.internals.validity.valid; const hasInteracted = this.hasInteracted; @@ -358,17 +353,17 @@ export class WebAwesomeFormAssociated */ setCustomValidity(message: string) { if (!message) { - this.__manualCustomError = false + this.__manualCustomError = false; this.setValidity({}); return; } - this.__manualCustomError = true + this.__manualCustomError = true; this.setValidity({ customError: true }, message, this.validationTarget); } formResetCallback() { - this.resetValidity() + this.resetValidity(); this.hasInteracted = false; this.valueHasChanged = false; this.emittedEvents = []; @@ -378,20 +373,20 @@ export class WebAwesomeFormAssociated formDisabledCallback(isDisabled: boolean) { this.disabled = isDisabled; - this.updateValidity() + this.updateValidity(); } /** * Called when the browser is trying to restore element’s state to state in which case reason is “restore”, or when the browser is trying to fulfill autofill on behalf of user in which case reason is “autocomplete”. In the case of “restore”, state is a string, File, or FormData object previously set as the second argument to setFormValue. */ - formStateRestoreCallback(state: string | File | FormData | null, reason: "autocomplete" | "restore") { - this.value = state + formStateRestoreCallback(state: string | File | FormData | null, reason: 'autocomplete' | 'restore') { + this.value = state; - if (reason === "restore") { - this.resetValidity() + if (reason === 'restore') { + this.resetValidity(); } - this.updateValidity() + this.updateValidity(); } setValue(...args: Parameters) { @@ -410,18 +405,18 @@ export class WebAwesomeFormAssociated /** * Reset validity is a way of removing manual custom errors and native validation. */ - resetValidity () { - this.setCustomValidity("") - this.setValidity({}) + resetValidity() { + this.setCustomValidity(''); + this.setValidity({}); } updateValidity() { if ( - this.disabled - || this.hasAttribute('disabled') - || !this.willValidate // + this.disabled || + this.hasAttribute('disabled') || + !this.willValidate // ) { - this.resetValidity() + this.resetValidity(); return; }