This commit is contained in:
konnorrogers
2024-05-07 19:42:50 -04:00
parent 1039d8e057
commit 1174200f20
20 changed files with 313 additions and 308 deletions

View File

@@ -206,43 +206,44 @@ describe('<wa-button>', () => {
expect(submitter.formNoValidate).to.be.true;
});
it("should only submit button name / value pair when the form is submitted", async () => {
const form = await fixture<HTMLFormElement>(html`<form>
<wa-button type="submit" name="btn-1" value="value-1">Button 1</wa-button>
<wa-button type="submit" name="btn-2" value="value-2">Button 2</wa-button>
</form>`);
it('should only submit button name / value pair when the form is submitted', async () => {
const form = await fixture<HTMLFormElement>(
html`<form>
<wa-button type="submit" name="btn-1" value="value-1">Button 1</wa-button>
<wa-button type="submit" name="btn-2" value="value-2">Button 2</wa-button>
</form>`
);
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', () => {

View File

@@ -188,7 +188,7 @@ describe('<wa-checkbox>', () => {
it('should be valid when required and checked', async () => {
const checkbox = await fixture<HTMLFormElement>(html` <wa-checkbox required checked></wa-checkbox> `);
await checkbox.updateComplete
await checkbox.updateComplete;
expect(checkbox.checkValidity()).to.be.true;
});

View File

@@ -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 `<form>` 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. */

View File

@@ -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<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();
}
render() {

View File

@@ -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('<wa-input>', () => {
it('should pass accessibility tests', async () => {
@@ -168,16 +168,18 @@ describe('<wa-input>', () => {
});
it('should not add a value to the form if disabled', async () => {
const form = await fixture<HTMLFormElement>(html` <form><wa-input name="name" disabled required></wa-input></form>`);
const el = form.querySelector("wa-input")!
el.value = "blah"
const form = await fixture<HTMLFormElement>(
html` <form><wa-input name="name" disabled required></wa-input></form>`
);
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('<wa-input>', () => {
});
});
it("Should be invalid if the pattern is invalid", async () => {
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();
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');
});

View File

@@ -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<WebAwesomeFormAssociated["formStateRestoreCallback"]>) {
const [value, reason] = args
super.formStateRestoreCallback(value, reason)
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
this.input.value = value;
}
formResetCallback () {
formResetCallback() {
this.input.value = this.defaultValue;
this.value = this.defaultValue;
super.formResetCallback()
super.formResetCallback();
}
render() {

View File

@@ -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;

View File

@@ -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');
});

View File

@@ -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<WaRadio | WaRadioButton>("wa-radio, wa-radio-button")
const clickedRadio = (e.target as HTMLElement).closest<WaRadio | WaRadioButton>('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<WaRadio | WaRadioButton>('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<WaRadio | WaRadioButton>(':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<WebAwesomeFormAssociated["formResetCallback"]>) {
this.value = this.defaultValue
formResetCallback(...args: Parameters<WebAwesomeFormAssociated['formResetCallback']>) {
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`
<slot @slotchange=${this.syncRadios}></slot>
`;
const defaultSlot = html` <slot @slotchange=${this.syncRadios}></slot> `;
return html`
<fieldset
@@ -331,3 +333,8 @@ export default class WaRadioGroup extends WebAwesomeFormAssociated {
}
}
declare global {
interface HTMLElementTagNameMap {
'wa-radio-group': WaRadioGroup;
}
}

View File

@@ -1,7 +1,7 @@
import '../icon/icon.js';
import { classMap } from 'lit/directives/class-map.js';
import { html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { html } from 'lit';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociated } from '../../internal/webawesome-element.js';
import componentStyles from '../../styles/component.styles.js';
@@ -38,7 +38,7 @@ import type { CSSResultGroup } from 'lit';
* @cssproperty --checked-icon-scale - The size of the checked icon relative to the radio.
* @cssproperty --toggle-size - The size of the radio.
*/
@customElement("wa-radio")
@customElement('wa-radio')
export default class WaRadio extends WebAwesomeFormAssociated {
static styles: CSSResultGroup = [componentStyles, styles];
@@ -48,11 +48,11 @@ export default class WaRadio extends WebAwesomeFormAssociated {
/**
* The string pointing to a form's id.
*/
@property({ reflect: true }) form: string | null = null
@property({ reflect: true }) form: string | null = null;
/** The radio's value. When selected, the radio group will receive this value. */
@property({ attribute: false }) value: string;
@property({ reflect: true, attribute: "value" }) defaultValue: string = ""
@property({ reflect: true, attribute: 'value' }) defaultValue: string = '';
/**
* The radio's size. When used inside a radio group, the size will be determined by the radio group's size so this
@@ -86,14 +86,14 @@ export default class WaRadio extends WebAwesomeFormAssociated {
private setInitialAttributes() {
this.setAttribute('role', 'radio');
this.tabIndex = 0
this.tabIndex = 0;
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
}
@watch('checked')
handleCheckedChange() {
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
this.tabIndex = this.checked ? 0 : -1
this.tabIndex = this.checked ? 0 : -1;
}
/**
@@ -148,4 +148,3 @@ declare global {
'wa-radio': WaRadio;
}
}

View File

@@ -43,14 +43,12 @@ import type { CSSResultGroup } from 'lit';
* @cssproperty --track-height - The height of the track.
* @cssproperty --track-active-offset - The point of origin of the active track.
*/
@customElement("wa-range")
@customElement('wa-range')
export default class WaRange extends WebAwesomeFormAssociated {
static styles: CSSResultGroup = [componentStyles, formControlStyles, styles];
static get validators () {
return [
MirrorValidator()
]
static get validators() {
return [MirrorValidator()];
}
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
@@ -71,7 +69,7 @@ export default class WaRange extends WebAwesomeFormAssociated {
@property({ attribute: false, type: Number }) value = 0;
/** The default value of the form control. Primarily used for resetting the form control. */
@property({ type: Number, attribute: "value", reflect: true }) defaultValue = 0;
@property({ type: Number, attribute: 'value', reflect: true }) defaultValue = 0;
/** The range's label. If you need to display HTML, use the `label` slot instead. */
@property() label = '';
@@ -191,7 +189,7 @@ export default class WaRange extends WebAwesomeFormAssociated {
// min, max, and step properly
this.input.value = this.value.toString();
this.value = parseFloat(this.input.value);
this.updateValidity()
this.updateValidity();
this.syncRange();
}

View File

@@ -118,9 +118,9 @@ describe('<wa-select>', () => {
</wa-select>
`);
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<WaOption>('wa-option')[1];
const changeHandler = sinon.spy();

View File

@@ -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`<wa-tag>+${this.selectedOptions.length - index}</wa-tag>`;
}
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() {

View File

@@ -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<WebAwesomeFormAssociated["formStateRestoreCallback"]>) {
const [value, reason] = args
super.formStateRestoreCallback(value, reason)
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
this.input.value = value;
}
formResetCallback () {
formResetCallback() {
this.input.value = this.defaultValue;
this.value = this.defaultValue;
super.formResetCallback()
super.formResetCallback();
}
render() {

View File

@@ -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,

View File

@@ -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();

View File

@@ -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<Validator["checkValidity"]> = {
message: "",
checkValidity(this: typeof GroupRequiredValidator, element) {
const validity: ReturnType<Validator['checkValidity']> = {
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;
};

View File

@@ -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;
}
}
};
};

View File

@@ -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<Validator["checkValidity"]> = {
message: "",
observedAttributes: ['required'],
message: 'Please fill out this field', // @TODO: Add a translation.
checkValidity(element) {
const validity: ReturnType<Validator['checkValidity']> = {
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;
};

View File

@@ -94,16 +94,14 @@ export default class WebAwesomeElement extends LitElement {
}
}
export interface Validator<
T extends WebAwesomeFormAssociated = WebAwesomeFormAssociated
> {
export interface Validator<T extends WebAwesomeFormAssociated = WebAwesomeFormAssociated> {
observedAttributes?: string[];
checkValidity: (element: T) => {
message: string;
isValid: boolean;
invalidKeys: Exclude<keyof ValidityState, 'valid'>[];
};
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<LitElement["firstUpdated"]>) {
super.firstUpdated(...args)
this.updateValidity()
firstUpdated(...args: Parameters<LitElement['firstUpdated']>) {
super.firstUpdated(...args);
this.updateValidity();
}
emitInvalid = (e: Event) => {
@@ -241,27 +238,25 @@ export class WebAwesomeFormAssociated
};
protected willUpdate(changedProperties: PropertyValues<this>) {
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 `<wa-select>`
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 elements 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<typeof this.internals.setFormValue>) {
@@ -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;
}