mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 12:09:26 +00:00
feat(form): add reset functionality (#799)
* feat(form): add reset functionality * feat(interal): add defaultValue decorator * feat: add defaultValue and defaultChecked * chore: implement unit tests * chore: remove leftover
This commit is contained in:
@@ -83,7 +83,7 @@ export default class SlButton extends LitElement {
|
||||
* The type of button. When the type is `submit`, the button will submit the surrounding form. Note that the default
|
||||
* value is `button` instead of `submit`, which is opposite of how native `<button>` elements behave.
|
||||
*/
|
||||
@property() type: 'button' | 'submit' = 'button';
|
||||
@property() type: 'button' | 'submit' | 'reset' = 'button';
|
||||
|
||||
/** An optional name for the button. Ignored when `href` is set. */
|
||||
@property() name?: string;
|
||||
@@ -153,6 +153,10 @@ export default class SlButton extends LitElement {
|
||||
if (this.type === 'submit') {
|
||||
this.formSubmitController.submit(this);
|
||||
}
|
||||
|
||||
if (this.type === 'reset') {
|
||||
this.formSubmitController.reset(this);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -121,6 +121,36 @@ describe('<sl-checkbox>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when resetting a form', () => {
|
||||
it('should reset the element to its initial value', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html`
|
||||
<form>
|
||||
<sl-checkbox name="a" value="1" checked></sl-checkbox>
|
||||
<sl-button type="reset">Reset</sl-button>
|
||||
</form>
|
||||
`);
|
||||
const button = form.querySelector('sl-button')!;
|
||||
const checkbox = form.querySelector('sl-checkbox')!;
|
||||
checkbox.checked = false;
|
||||
|
||||
await checkbox.updateComplete;
|
||||
setTimeout(() => button.click());
|
||||
|
||||
await oneEvent(form, 'reset');
|
||||
await checkbox.updateComplete;
|
||||
|
||||
expect(checkbox.checked).to.true;
|
||||
|
||||
checkbox.defaultChecked = false;
|
||||
|
||||
setTimeout(() => button.click());
|
||||
await oneEvent(form, 'reset');
|
||||
await checkbox.updateComplete;
|
||||
|
||||
expect(checkbox.checked).to.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('click', () => {
|
||||
it('should click the inner input', async () => {
|
||||
const el = await fixture<SlCheckbox>(html`<sl-checkbox></sl-checkbox>`);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { live } from 'lit/directives/live.js';
|
||||
import { defaultValue } from '../../internal/default-value';
|
||||
import { emit } from '../../internal/event';
|
||||
import { FormSubmitController } from '../../internal/form';
|
||||
import { watch } from '../../internal/watch';
|
||||
@@ -32,7 +33,9 @@ export default class SlCheckbox extends LitElement {
|
||||
|
||||
// @ts-expect-error -- Controller is currently unused
|
||||
private readonly formSubmitController = new FormSubmitController(this, {
|
||||
value: (control: SlCheckbox) => (control.checked ? control.value || 'on' : undefined)
|
||||
value: (control: SlCheckbox) => (control.checked ? control.value || 'on' : undefined),
|
||||
defaultValue: (control: SlCheckbox) => control.defaultChecked,
|
||||
setValue: (control: SlCheckbox, checked: boolean) => (control.checked = checked)
|
||||
});
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@@ -58,6 +61,10 @@ export default class SlCheckbox extends LitElement {
|
||||
/** This will be true when the control is in an invalid state. Validity is determined by the `required` prop. */
|
||||
@property({ type: Boolean, reflect: true }) invalid = false;
|
||||
|
||||
/** Gets or sets the default value used to reset this element. The initial value corresponds to the one originally specified in the HTML that created this element. */
|
||||
@defaultValue('checked')
|
||||
defaultChecked = false;
|
||||
|
||||
firstUpdated() {
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import type SlColorPicker from './color-picker';
|
||||
|
||||
@@ -45,4 +45,34 @@ describe('<sl-color-picker>', () => {
|
||||
|
||||
expect(trigger?.style.color).to.equal('rgb(0, 0, 0)');
|
||||
});
|
||||
|
||||
describe('when resetting a form', () => {
|
||||
it('should reset the element to its initial value', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html`
|
||||
<form>
|
||||
<sl-color-picker name="a" value="#FFFFFF"></sl-color-picker>
|
||||
<sl-button type="reset">Reset</sl-button>
|
||||
</form>
|
||||
`);
|
||||
const button = form.querySelector('sl-button')!;
|
||||
const colorPicker = form.querySelector('sl-color-picker')!;
|
||||
colorPicker.value = '#000000';
|
||||
|
||||
await colorPicker.updateComplete;
|
||||
|
||||
setTimeout(() => button.click());
|
||||
await oneEvent(form, 'reset');
|
||||
await colorPicker.updateComplete;
|
||||
|
||||
expect(colorPicker.value).to.equal('#FFFFFF');
|
||||
|
||||
colorPicker.defaultValue = '';
|
||||
|
||||
setTimeout(() => button.click());
|
||||
await oneEvent(form, 'reset');
|
||||
await colorPicker.updateComplete;
|
||||
|
||||
expect(colorPicker.value).to.equal('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import '../../components/dropdown/dropdown';
|
||||
import '../../components/icon/icon';
|
||||
import '../../components/input/input';
|
||||
import '../../components/visually-hidden/visually-hidden';
|
||||
import { defaultValue } from '../../internal/default-value';
|
||||
import { drag } from '../../internal/drag';
|
||||
import { emit } from '../../internal/event';
|
||||
import { FormSubmitController } from '../../internal/form';
|
||||
@@ -105,6 +106,10 @@ export default class SlColorPicker extends LitElement {
|
||||
/** The current color. */
|
||||
@property() value = '';
|
||||
|
||||
/** Gets or sets the default value used to reset this element. The initial value corresponds to the one originally specified in the HTML that created this element. */
|
||||
@defaultValue()
|
||||
defaultValue = '';
|
||||
|
||||
/* The color picker's label. This will not be displayed, but it will be announced by assistive devices. */
|
||||
@property() label = '';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import sinon from 'sinon';
|
||||
import { serialize } from '../../utilities/form';
|
||||
@@ -124,6 +124,36 @@ describe('<sl-input>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when resetting a form', () => {
|
||||
it('should reset the element to its initial value', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html`
|
||||
<form>
|
||||
<sl-input name="a" value="test"></sl-input>
|
||||
<sl-button type="reset">Reset</sl-button>
|
||||
</form>
|
||||
`);
|
||||
const button = form.querySelector('sl-button')!;
|
||||
const input = form.querySelector('sl-input')!;
|
||||
input.value = '1234';
|
||||
|
||||
await input.updateComplete;
|
||||
|
||||
setTimeout(() => button.click());
|
||||
await oneEvent(form, 'reset');
|
||||
await input.updateComplete;
|
||||
|
||||
expect(input.value).to.equal('test');
|
||||
|
||||
input.defaultValue = '';
|
||||
|
||||
setTimeout(() => button.click());
|
||||
await oneEvent(form, 'reset');
|
||||
await input.updateComplete;
|
||||
|
||||
expect(input.value).to.equal('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when calling HTMLFormElement.reportValidity()', () => {
|
||||
it('should be invalid when the input is empty and form.reportValidity() is called', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html`
|
||||
|
||||
@@ -3,6 +3,7 @@ import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { live } from 'lit/directives/live.js';
|
||||
import { defaultValue } from '../../internal/default-value';
|
||||
import '../../components/icon/icon';
|
||||
import { emit } from '../../internal/event';
|
||||
import { FormSubmitController } from '../../internal/form';
|
||||
@@ -77,6 +78,10 @@ export default class SlInput extends LitElement {
|
||||
/** The input's value attribute. */
|
||||
@property() value = '';
|
||||
|
||||
/** Gets or sets the default value used to reset this element. The initial value corresponds to the one originally specified in the HTML that created this element. */
|
||||
@defaultValue()
|
||||
defaultValue = '';
|
||||
|
||||
/** Draws a filled input. */
|
||||
@property({ type: Boolean, reflect: true }) filled = false;
|
||||
|
||||
|
||||
@@ -97,6 +97,38 @@ describe('<sl-radio-button>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when resetting a form', () => {
|
||||
it('should reset the element to its initial value', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html`
|
||||
<form>
|
||||
<sl-radio-group>
|
||||
<sl-radio-button id="radio-1" name="a" value="1" checked></sl-radio-button>
|
||||
<sl-radio-button id="radio-2" name="a" value="2"></sl-radio-button>
|
||||
<sl-radio-button id="radio-3" name="a" value="3"></sl-radio-button>
|
||||
</sl-radio-group>
|
||||
<sl-button type="reset">Reset</sl-button>
|
||||
</form>
|
||||
`);
|
||||
const button = form.querySelector('sl-button')!;
|
||||
const radio1: SlRadioButton = form.querySelector('#radio-1')!;
|
||||
const radio2: SlRadioButton = form.querySelector('#radio-2')!;
|
||||
|
||||
radio2.click();
|
||||
await radio2.updateComplete;
|
||||
|
||||
expect(radio2.checked).to.be.true;
|
||||
expect(radio1.checked).to.be.false;
|
||||
|
||||
setTimeout(() => button.click());
|
||||
|
||||
await oneEvent(form, 'reset');
|
||||
await radio1.updateComplete;
|
||||
|
||||
expect(radio1.checked).to.true;
|
||||
expect(radio2.checked).to.false;
|
||||
});
|
||||
});
|
||||
|
||||
it('should show a constraint validation error when setCustomValidity() is called', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html`
|
||||
<form>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
import { defaultValue } from '../../internal/default-value';
|
||||
import { emit } from '../../internal/event';
|
||||
import { FormSubmitController } from '../../internal/form';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
@@ -37,7 +38,9 @@ export default class SlRadioButton extends LitElement {
|
||||
@query('.hidden-input') hiddenInput: HTMLInputElement;
|
||||
|
||||
protected readonly formSubmitController = new FormSubmitController(this, {
|
||||
value: (control: SlRadioButton) => (control.checked ? control.value : undefined)
|
||||
value: (control: SlRadioButton) => (control.checked ? control.value : undefined),
|
||||
defaultValue: (control: SlRadioButton) => control.defaultChecked,
|
||||
setValue: (control: SlRadioButton, checked: boolean) => (control.checked = checked)
|
||||
});
|
||||
private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
|
||||
|
||||
@@ -61,6 +64,10 @@ export default class SlRadioButton extends LitElement {
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) invalid = false;
|
||||
|
||||
/** Gets or sets the default value used to reset this element. The initial value corresponds to the one originally specified in the HTML that created this element. */
|
||||
@defaultValue('checked')
|
||||
defaultChecked = false;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.setAttribute('role', 'radio');
|
||||
|
||||
@@ -98,6 +98,36 @@ describe('<sl-radio>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when resetting a form', () => {
|
||||
it('should reset the element to its initial value', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html`
|
||||
<form>
|
||||
<sl-radio name="a" value="1" checked></sl-radio>
|
||||
<sl-button type="reset">Reset</sl-button>
|
||||
</form>
|
||||
`);
|
||||
const button = form.querySelector('sl-button')!;
|
||||
const radio = form.querySelector('sl-radio')!;
|
||||
radio.checked = false;
|
||||
|
||||
await radio.updateComplete;
|
||||
setTimeout(() => button.click());
|
||||
|
||||
await oneEvent(form, 'reset');
|
||||
await radio.updateComplete;
|
||||
|
||||
expect(radio.checked).to.true;
|
||||
|
||||
radio.defaultChecked = false;
|
||||
|
||||
setTimeout(() => button.click());
|
||||
await oneEvent(form, 'reset');
|
||||
await radio.updateComplete;
|
||||
|
||||
expect(radio.checked).to.false;
|
||||
});
|
||||
});
|
||||
|
||||
it('should submit "on" when no value is provided', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html`
|
||||
<form>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { live } from 'lit/directives/live.js';
|
||||
import { defaultValue } from '../../internal/default-value';
|
||||
import { emit } from '../../internal/event';
|
||||
import { FormSubmitController } from '../../internal/form';
|
||||
import { watch } from '../../internal/watch';
|
||||
@@ -30,7 +31,9 @@ export default class SlRadio extends LitElement {
|
||||
@query('.radio__input') input: HTMLInputElement;
|
||||
|
||||
protected readonly formSubmitController = new FormSubmitController(this, {
|
||||
value: (control: HTMLInputElement) => (control.checked ? control.value || 'on' : undefined)
|
||||
value: (control: SlRadio) => (control.checked ? control.value || 'on' : undefined),
|
||||
defaultValue: (control: SlRadio) => control.defaultChecked,
|
||||
setValue: (control: SlRadio, checked: boolean) => (control.checked = checked)
|
||||
});
|
||||
|
||||
@state() protected hasFocus = false;
|
||||
@@ -53,6 +56,10 @@ export default class SlRadio extends LitElement {
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) invalid = false;
|
||||
|
||||
/** Gets or sets the default value used to reset this element. The initial value corresponds to the one originally specified in the HTML that created this element. */
|
||||
@defaultValue('checked')
|
||||
defaultChecked = false;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.setAttribute('role', 'radio');
|
||||
|
||||
74
src/components/range/range.test.ts
Normal file
74
src/components/range/range.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import { serialize } from '../../utilities/form';
|
||||
import type SlRange from './range';
|
||||
|
||||
describe('<sl-range>', () => {
|
||||
it('should pass accessibility tests', async () => {
|
||||
const el = await fixture<SlRange>(html` <sl-range label="Name"></sl-range> `);
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should be disabled with the disabled attribute', async () => {
|
||||
const el = await fixture<SlRange>(html` <sl-range disabled></sl-range> `);
|
||||
const input = el.shadowRoot!.querySelector<HTMLInputElement>('[part="input"]')!;
|
||||
|
||||
expect(input.disabled).to.be.true;
|
||||
});
|
||||
|
||||
it('should focus the input when clicking on the label', async () => {
|
||||
const el = await fixture<SlRange>(html` <sl-range label="Name"></sl-range> `);
|
||||
const label = el.shadowRoot!.querySelector('[part="form-control-label"]')!;
|
||||
const submitHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-focus', submitHandler);
|
||||
(label as HTMLLabelElement).click();
|
||||
await waitUntil(() => submitHandler.calledOnce);
|
||||
|
||||
expect(submitHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
describe('when serializing', () => {
|
||||
it('should serialize its name and value with FormData', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html` <form><sl-range name="a" value="1"></sl-range></form> `);
|
||||
const formData = new FormData(form);
|
||||
expect(formData.get('a')).to.equal('1');
|
||||
});
|
||||
|
||||
it('should serialize its name and value with JSON', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html` <form><sl-range name="a" value="1"></sl-range></form> `);
|
||||
const json = serialize(form);
|
||||
expect(json.a).to.equal('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when resetting a form', () => {
|
||||
it('should reset the element to its initial value', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html`
|
||||
<form>
|
||||
<sl-range name="a" value="99"></sl-range>
|
||||
<sl-button type="reset">Reset</sl-button>
|
||||
</form>
|
||||
`);
|
||||
const button = form.querySelector('sl-button')!;
|
||||
const input = form.querySelector('sl-range')!;
|
||||
input.value = 80;
|
||||
|
||||
await input.updateComplete;
|
||||
|
||||
setTimeout(() => button.click());
|
||||
await oneEvent(form, 'reset');
|
||||
await input.updateComplete;
|
||||
|
||||
expect(input.value).to.equal(99);
|
||||
|
||||
input.defaultValue = 0;
|
||||
|
||||
setTimeout(() => button.click());
|
||||
await oneEvent(form, 'reset');
|
||||
await input.updateComplete;
|
||||
|
||||
expect(input.value).to.equal(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { live } from 'lit/directives/live.js';
|
||||
import { defaultValue } from '../../internal/default-value';
|
||||
import { emit } from '../../internal/event';
|
||||
import { FormSubmitController } from '../../internal/form';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
@@ -87,6 +88,10 @@ export default class SlRange extends LitElement {
|
||||
/** A function used to format the tooltip's value. */
|
||||
@property({ attribute: false }) tooltipFormatter: (value: number) => string = (value: number) => value.toString();
|
||||
|
||||
/** Gets or sets the default value used to reset this element. The initial value corresponds to the one originally specified in the HTML that created this element. */
|
||||
@defaultValue()
|
||||
defaultValue = 0;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.resizeObserver = new ResizeObserver(() => this.syncRange());
|
||||
|
||||
@@ -130,4 +130,33 @@ describe('<sl-select>', () => {
|
||||
|
||||
expect(displayLabel.textContent?.trim()).to.equal('updated');
|
||||
});
|
||||
|
||||
describe('when resetting a form', () => {
|
||||
it('should reset the element to its initial value', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html`
|
||||
<form>
|
||||
<sl-select value="option-1">
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
</sl-select>
|
||||
<sl-button type="reset">Reset</sl-button>
|
||||
</form>
|
||||
`);
|
||||
const button = form.querySelector('sl-button')!;
|
||||
const select = form.querySelector('sl-select')!;
|
||||
const option2 = form.querySelectorAll('sl-menu-item')![1];
|
||||
|
||||
option2.click();
|
||||
await option2.updateComplete;
|
||||
|
||||
expect(select.value).to.equal('option-2');
|
||||
|
||||
setTimeout(() => button.click());
|
||||
await oneEvent(form, 'reset');
|
||||
await select.updateComplete;
|
||||
|
||||
expect(select.value).to.equal('option-1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { html, LitElement } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { defaultValue } from '../../internal/default-value';
|
||||
import '../../components/dropdown/dropdown';
|
||||
import '../../components/icon-button/icon-button';
|
||||
import '../../components/icon/icon';
|
||||
@@ -137,6 +138,10 @@ export default class SlSelect extends LitElement {
|
||||
/** This will be true when the control is in an invalid state. Validity is determined by the `required` prop. */
|
||||
@property({ type: Boolean, reflect: true }) invalid = false;
|
||||
|
||||
/** Gets or sets the default value used to reset this element. The initial value corresponds to the one originally specified in the HTML that created this element. */
|
||||
@defaultValue()
|
||||
defaultValue = '';
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.resizeObserver = new ResizeObserver(() => this.resizeMenu());
|
||||
|
||||
@@ -59,4 +59,34 @@ describe('<sl-switch>', () => {
|
||||
el.checked = false;
|
||||
await el.updateComplete;
|
||||
});
|
||||
|
||||
describe('when resetting a form', () => {
|
||||
it('should reset the element to its initial value', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html`
|
||||
<form>
|
||||
<sl-switch name="a" value="1" checked></sl-switch>
|
||||
<sl-button type="reset">Reset</sl-button>
|
||||
</form>
|
||||
`);
|
||||
const button = form.querySelector('sl-button')!;
|
||||
const switchEl = form.querySelector('sl-switch')!;
|
||||
switchEl.checked = false;
|
||||
|
||||
await switchEl.updateComplete;
|
||||
setTimeout(() => button.click());
|
||||
|
||||
await oneEvent(form, 'reset');
|
||||
await switchEl.updateComplete;
|
||||
|
||||
expect(switchEl.checked).to.true;
|
||||
|
||||
switchEl.defaultChecked = false;
|
||||
|
||||
setTimeout(() => button.click());
|
||||
await oneEvent(form, 'reset');
|
||||
await switchEl.updateComplete;
|
||||
|
||||
expect(switchEl.checked).to.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { live } from 'lit/directives/live.js';
|
||||
import { defaultValue } from '../../internal/default-value';
|
||||
import { emit } from '../../internal/event';
|
||||
import { FormSubmitController } from '../../internal/form';
|
||||
import { watch } from '../../internal/watch';
|
||||
@@ -35,7 +36,9 @@ export default class SlSwitch extends LitElement {
|
||||
|
||||
// @ts-expect-error -- Controller is currently unused
|
||||
private readonly formSubmitController = new FormSubmitController(this, {
|
||||
value: (control: SlSwitch) => (control.checked ? control.value : undefined)
|
||||
value: (control: SlSwitch) => (control.checked ? control.value : undefined),
|
||||
defaultValue: (control: SlSwitch) => control.defaultChecked,
|
||||
setValue: (control: SlSwitch, checked: boolean) => (control.checked = checked)
|
||||
});
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@@ -58,6 +61,10 @@ export default class SlSwitch extends LitElement {
|
||||
/** This will be true when the control is in an invalid state. Validity is determined by the `required` prop. */
|
||||
@property({ type: Boolean, reflect: true }) invalid = false;
|
||||
|
||||
/** Gets or sets the default value used to reset this element. The initial value corresponds to the one originally specified in the HTML that created this element. */
|
||||
@defaultValue('checked')
|
||||
defaultChecked = false;
|
||||
|
||||
firstUpdated() {
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import { serialize } from '../../utilities/form';
|
||||
import type SlTextarea from './textarea';
|
||||
@@ -71,4 +71,34 @@ describe('<sl-textarea>', () => {
|
||||
expect(json.a).to.equal('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when resetting a form', () => {
|
||||
it('should reset the element to its initial value', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html`
|
||||
<form>
|
||||
<sl-textarea name="a" value="test"></sl-textarea>
|
||||
<sl-button type="reset">Reset</sl-button>
|
||||
</form>
|
||||
`);
|
||||
const button = form.querySelector('sl-button')!;
|
||||
const textarea = form.querySelector('sl-textarea')!;
|
||||
textarea.value = '1234';
|
||||
|
||||
await textarea.updateComplete;
|
||||
|
||||
setTimeout(() => button.click());
|
||||
await oneEvent(form, 'reset');
|
||||
await textarea.updateComplete;
|
||||
|
||||
expect(textarea.value).to.equal('test');
|
||||
|
||||
textarea.defaultValue = '';
|
||||
|
||||
setTimeout(() => button.click());
|
||||
await oneEvent(form, 'reset');
|
||||
await textarea.updateComplete;
|
||||
|
||||
expect(textarea.value).to.equal('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { live } from 'lit/directives/live.js';
|
||||
import { defaultValue } from '../../internal/default-value';
|
||||
import { emit } from '../../internal/event';
|
||||
import { FormSubmitController } from '../../internal/form';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
@@ -113,6 +114,10 @@ export default class SlTextarea extends LitElement {
|
||||
/** The textarea's inputmode attribute. */
|
||||
@property() inputmode: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
|
||||
|
||||
/** Gets or sets the default value used to reset this element. The initial value corresponds to the one originally specified in the HTML that created this element. */
|
||||
@defaultValue()
|
||||
defaultValue = '';
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.resizeObserver = new ResizeObserver(() => this.setTextareaHeight());
|
||||
|
||||
51
src/internal/default-value.ts
Normal file
51
src/internal/default-value.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// @defaultValue decorator
|
||||
//
|
||||
// Runs when the corresponding attribute of the observed property changes, e.g. after calling Element.setAttribute or after updating
|
||||
// the observed property.
|
||||
//
|
||||
// The decorator checks whether the value of the attribute is different from the value of the property and in that case
|
||||
// it saves the new value.
|
||||
//
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// @property({ type: Boolean, reflect: true })
|
||||
// checked = false;
|
||||
//
|
||||
// @defaultValue('checked')
|
||||
// defaultChecked = false;
|
||||
//
|
||||
|
||||
import { defaultConverter } from 'lit';
|
||||
import type { ReactiveElement } from 'lit';
|
||||
|
||||
export const defaultValue =
|
||||
(propertyName = 'value') =>
|
||||
(proto: ReactiveElement, key: string) => {
|
||||
const ctor = proto.constructor as typeof ReactiveElement;
|
||||
|
||||
const attributeChangedCallback = ctor.prototype.attributeChangedCallback;
|
||||
ctor.prototype.attributeChangedCallback = function (
|
||||
this: ReactiveElement & { [name: string]: unknown },
|
||||
name,
|
||||
old,
|
||||
value
|
||||
) {
|
||||
const options = ctor.getPropertyOptions(propertyName);
|
||||
const attributeName = typeof options.attribute === 'string' ? options.attribute : propertyName;
|
||||
|
||||
if (name === attributeName) {
|
||||
const converter = options.converter || defaultConverter;
|
||||
const fromAttribute =
|
||||
typeof converter === 'function' ? converter : converter?.fromAttribute ?? defaultConverter.fromAttribute;
|
||||
|
||||
const newValue: unknown = fromAttribute!(value, options.type);
|
||||
|
||||
if (this[propertyName] !== newValue) {
|
||||
this[key] = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
attributeChangedCallback.call(this, name, old, value);
|
||||
};
|
||||
};
|
||||
@@ -11,6 +11,8 @@ export interface FormSubmitControllerOptions {
|
||||
name: (input: unknown) => string;
|
||||
/** A function that returns the form control's current value. */
|
||||
value: (input: unknown) => unknown | unknown[];
|
||||
/** A function that returns the form control's default value. */
|
||||
defaultValue: (input: unknown) => unknown | unknown[];
|
||||
/** A function that returns the form control's current disabled state. If disabled, the value won't be submitted. */
|
||||
disabled: (input: unknown) => boolean;
|
||||
/**
|
||||
@@ -18,6 +20,9 @@ export interface FormSubmitControllerOptions {
|
||||
* prevent submission and trigger the browser's constraint violation warning.
|
||||
*/
|
||||
reportValidity: (input: unknown) => boolean;
|
||||
|
||||
/** A function that sets the form control's value */
|
||||
setValue: (input: unknown, value: unknown) => void;
|
||||
}
|
||||
|
||||
export class FormSubmitController implements ReactiveController {
|
||||
@@ -31,14 +36,19 @@ export class FormSubmitController implements ReactiveController {
|
||||
form: (input: HTMLInputElement) => input.closest('form'),
|
||||
name: (input: HTMLInputElement) => input.name,
|
||||
value: (input: HTMLInputElement) => input.value,
|
||||
defaultValue: (input: HTMLInputElement) => input.defaultValue,
|
||||
disabled: (input: HTMLInputElement) => input.disabled,
|
||||
reportValidity: (input: HTMLInputElement) => {
|
||||
return typeof input.reportValidity === 'function' ? input.reportValidity() : true;
|
||||
},
|
||||
setValue: (input: HTMLInputElement, value: string) => {
|
||||
input.value = value;
|
||||
},
|
||||
...options
|
||||
};
|
||||
this.handleFormData = this.handleFormData.bind(this);
|
||||
this.handleFormSubmit = this.handleFormSubmit.bind(this);
|
||||
this.handleFormReset = this.handleFormReset.bind(this);
|
||||
this.reportFormValidity = this.reportFormValidity.bind(this);
|
||||
}
|
||||
|
||||
@@ -48,6 +58,7 @@ export class FormSubmitController implements ReactiveController {
|
||||
if (this.form) {
|
||||
this.form.addEventListener('formdata', this.handleFormData);
|
||||
this.form.addEventListener('submit', this.handleFormSubmit);
|
||||
this.form.addEventListener('reset', this.handleFormReset);
|
||||
|
||||
// Overload the form's reportValidity() method so it looks at Shoelace form controls
|
||||
if (!reportValidityOverloads.has(this.form)) {
|
||||
@@ -61,6 +72,7 @@ export class FormSubmitController implements ReactiveController {
|
||||
if (this.form) {
|
||||
this.form.removeEventListener('formdata', this.handleFormData);
|
||||
this.form.removeEventListener('submit', this.handleFormSubmit);
|
||||
this.form.removeEventListener('reset', this.handleFormReset);
|
||||
|
||||
// Remove the overload and restore the original method
|
||||
if (reportValidityOverloads.has(this.form)) {
|
||||
@@ -98,6 +110,10 @@ export class FormSubmitController implements ReactiveController {
|
||||
}
|
||||
}
|
||||
|
||||
handleFormReset() {
|
||||
this.options.setValue(this.host, this.options.defaultValue(this.host));
|
||||
}
|
||||
|
||||
reportFormValidity() {
|
||||
//
|
||||
// Shoelace form controls work hard to act like regular form controls. They support the Constraint Validation API
|
||||
@@ -128,13 +144,10 @@ export class FormSubmitController implements ReactiveController {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Submits the form, triggering validation and form data injection. */
|
||||
submit(submitter?: HTMLInputElement | SlButton) {
|
||||
// Calling form.submit() bypasses the submit event and constraint validation. To prevent this, we can inject a
|
||||
// native submit button into the form, "click" it, then remove it to simulate a standard form submission.
|
||||
doAction(type: 'submit' | 'reset', invoker?: HTMLInputElement | SlButton) {
|
||||
if (this.form) {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'submit';
|
||||
button.type = type;
|
||||
button.style.position = 'absolute';
|
||||
button.style.width = '0';
|
||||
button.style.height = '0';
|
||||
@@ -143,10 +156,10 @@ export class FormSubmitController implements ReactiveController {
|
||||
button.style.whiteSpace = 'nowrap';
|
||||
|
||||
// Pass form attributes through to the temporary button
|
||||
if (submitter) {
|
||||
if (invoker) {
|
||||
['formaction', 'formmethod', 'formnovalidate', 'formtarget'].forEach(attr => {
|
||||
if (submitter.hasAttribute(attr)) {
|
||||
button.setAttribute(attr, submitter.getAttribute(attr)!);
|
||||
if (invoker.hasAttribute(attr)) {
|
||||
button.setAttribute(attr, invoker.getAttribute(attr)!);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -156,4 +169,16 @@ export class FormSubmitController implements ReactiveController {
|
||||
button.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/** Resets the form, restoring all the control to their default value */
|
||||
reset(invoker?: HTMLInputElement | SlButton) {
|
||||
this.doAction('reset', invoker);
|
||||
}
|
||||
|
||||
/** Submits the form, triggering validation and form data injection. */
|
||||
submit(invoker?: HTMLInputElement | SlButton) {
|
||||
// Calling form.submit() bypasses the submit event and constraint validation. To prevent this, we can inject a
|
||||
// native submit button into the form, "click" it, then remove it to simulate a standard form submission.
|
||||
this.doAction('submit', invoker);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user