mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 04:09:12 +00:00
rename FormSubmitController; remove this.invalid
This commit is contained in:
@@ -19,6 +19,8 @@ New versions of Shoelace are released as-needed and generally occur when a criti
|
||||
- Fixed a bug in `<sl-color-picker>` that caused selected colors to be wrong due to incorrect HSV calculations
|
||||
- Fixed a bug in `<sl-radio-button>` that caused the checked button's right border to be incorrect [#1110](https://github.com/shoelace-style/shoelace/issues/1110)
|
||||
- Fixed a bug in `<sl-spinner>` that caused the animation to stop working correctly in Safari [#1121](https://github.com/shoelace-style/shoelace/issues/1121)
|
||||
- Refactored the `ShoelaceFormControl` interface to remove the `invalid` property, allowing a more intuitive API for controlling validation internally
|
||||
- Renamed the internal `FormSubmitController` to `FormControlController` to better reflect what it's used for
|
||||
|
||||
## 2.0.0-beta.88
|
||||
|
||||
|
||||
@@ -2,7 +2,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, literal } from 'lit/static-html.js';
|
||||
import { FormSubmitController } from '../../internal/form';
|
||||
import { FormControlController } from '../../internal/form';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { watch } from '../../internal/watch';
|
||||
@@ -39,7 +39,7 @@ import type { CSSResultGroup } from 'lit';
|
||||
export default class SlButton extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
private readonly formSubmitController = new FormSubmitController(this, {
|
||||
private readonly formControlController = new FormControlController(this, {
|
||||
form: input => {
|
||||
// Buttons support a form attribute that points to an arbitrary form, so if this attribute it set we need to query
|
||||
// the form from the same root using its id
|
||||
@@ -141,7 +141,7 @@ export default class SlButton extends ShoelaceElement implements ShoelaceFormCon
|
||||
|
||||
firstUpdated() {
|
||||
if (this.isButton()) {
|
||||
this.invalid = !(this.button as HTMLButtonElement).checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,11 +163,11 @@ export default class SlButton extends ShoelaceElement implements ShoelaceFormCon
|
||||
}
|
||||
|
||||
if (this.type === 'submit') {
|
||||
this.formSubmitController.submit(this);
|
||||
this.formControlController.submit(this);
|
||||
}
|
||||
|
||||
if (this.type === 'reset') {
|
||||
this.formSubmitController.reset(this);
|
||||
this.formControlController.reset(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,10 +181,9 @@ export default class SlButton extends ShoelaceElement implements ShoelaceFormCon
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
||||
if (this.isButton()) {
|
||||
this.button.disabled = this.disabled;
|
||||
this.invalid = !(this.button as HTMLButtonElement).checkValidity();
|
||||
// Disabled form controls are always valid
|
||||
this.formControlController.setValidity(this.disabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +224,7 @@ export default class SlButton extends ShoelaceElement implements ShoelaceFormCon
|
||||
setCustomValidity(message: string) {
|
||||
if (this.isButton()) {
|
||||
(this.button as HTMLButtonElement).setCustomValidity(message);
|
||||
this.invalid = !(this.button as HTMLButtonElement).checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 { FormSubmitController } from '../../internal/form';
|
||||
import { FormControlController } from '../../internal/form';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { watch } from '../../internal/watch';
|
||||
import '../icon/icon';
|
||||
@@ -39,8 +39,7 @@ import type { CSSResultGroup } from 'lit';
|
||||
export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
// @ts-expect-error - Controller is currently unused
|
||||
private readonly formSubmitController = new FormSubmitController(this, {
|
||||
private readonly formControlController = new FormControlController(this, {
|
||||
value: (control: SlCheckbox) => (control.checked ? control.value || 'on' : undefined),
|
||||
defaultValue: (control: SlCheckbox) => control.defaultChecked,
|
||||
setValue: (control: SlCheckbox, checked: boolean) => (control.checked = checked)
|
||||
@@ -49,7 +48,6 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
|
||||
@query('input[type="checkbox"]') input: HTMLInputElement;
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@state() invalid = false;
|
||||
|
||||
@property() title = ''; // make reactive to pass through
|
||||
|
||||
@@ -81,7 +79,7 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
|
||||
@defaultValue('checked') defaultChecked = false;
|
||||
|
||||
firstUpdated() {
|
||||
this.invalid = !this.checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
private handleClick() {
|
||||
@@ -106,16 +104,15 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
||||
this.input.disabled = this.disabled;
|
||||
this.invalid = !this.checkValidity();
|
||||
// Disabled form controls are always valid
|
||||
this.formControlController.setValidity(this.disabled);
|
||||
}
|
||||
|
||||
@watch(['checked', 'indeterminate'], { waitUntilFirstUpdate: true })
|
||||
handleStateChange() {
|
||||
this.input.checked = this.checked; // force a sync update
|
||||
this.input.indeterminate = this.indeterminate; // force a sync update
|
||||
this.invalid = !this.checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
/** Simulates a click on the checkbox. */
|
||||
@@ -149,7 +146,7 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
|
||||
*/
|
||||
setCustomValidity(message: string) {
|
||||
this.input.setCustomValidity(message);
|
||||
this.invalid = !this.checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { defaultValue } from '../../internal/default-value';
|
||||
import { drag } from '../../internal/drag';
|
||||
import { FormSubmitController } from '../../internal/form';
|
||||
import { FormControlController } from '../../internal/form';
|
||||
import { clamp } from '../../internal/math';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { watch } from '../../internal/watch';
|
||||
@@ -88,8 +88,7 @@ declare const EyeDropper: EyeDropperConstructor;
|
||||
export default class SlColorPicker extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
// @ts-expect-error - Controller is currently unused
|
||||
private readonly formSubmitController = new FormSubmitController(this);
|
||||
private readonly formControlController = new FormControlController(this);
|
||||
private isSafeValue = false;
|
||||
private lastValueEmitted: string;
|
||||
private readonly localize = new LocalizeController(this);
|
||||
@@ -105,7 +104,6 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
@state() private saturation = 100;
|
||||
@state() private brightness = 100;
|
||||
@state() private alpha = 100;
|
||||
@state() invalid = false;
|
||||
|
||||
/**
|
||||
* The current value of the color picker. The value's format will vary based the `format` attribute. To get the value
|
||||
@@ -679,7 +677,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
reportValidity() {
|
||||
if (!this.inline && this.input.invalid) {
|
||||
if (!this.inline && !this.checkValidity()) {
|
||||
// If the input is inline and invalid, show the dropdown so the browser can focus on it
|
||||
this.dropdown.show();
|
||||
this.addEventListener('sl-after-show', () => this.input.reportValidity(), { once: true });
|
||||
@@ -692,7 +690,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
|
||||
setCustomValidity(message: string) {
|
||||
this.input.setCustomValidity(message);
|
||||
this.invalid = this.input.invalid;
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 { FormSubmitController } from '../../internal/form';
|
||||
import { FormControlController } from '../../internal/form';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { watch } from '../../internal/watch';
|
||||
@@ -63,14 +63,13 @@ const isFirefox = isChromium ? false : navigator.userAgent.includes('Firefox');
|
||||
export default class SlInput extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
private readonly formSubmitController = new FormSubmitController(this);
|
||||
private readonly formControlController = new FormControlController(this);
|
||||
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@query('.input__control') input: HTMLInputElement;
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@state() invalid = false;
|
||||
@property() title = ''; // make reactive to pass through
|
||||
|
||||
/**
|
||||
@@ -220,7 +219,7 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.invalid = !this.checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
private handleBlur() {
|
||||
@@ -250,12 +249,12 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
|
||||
private handleInput() {
|
||||
this.value = this.input.value;
|
||||
this.invalid = !this.checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
this.emit('sl-input');
|
||||
}
|
||||
|
||||
private handleInvalid() {
|
||||
this.invalid = true;
|
||||
this.formControlController.setValidity(false);
|
||||
}
|
||||
|
||||
private handleKeyDown(event: KeyboardEvent) {
|
||||
@@ -272,7 +271,7 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
// See https://github.com/shoelace-style/shoelace/pull/988
|
||||
//
|
||||
if (!event.defaultPrevented && !event.isComposing) {
|
||||
this.formSubmitController.submit();
|
||||
this.formControlController.submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -284,9 +283,8 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
||||
this.input.disabled = this.disabled;
|
||||
this.invalid = !this.checkValidity();
|
||||
// Disabled form controls are always valid
|
||||
this.formControlController.setValidity(this.disabled);
|
||||
}
|
||||
|
||||
@watch('step', { waitUntilFirstUpdate: true })
|
||||
@@ -294,13 +292,13 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
// 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.invalid = !this.checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
@watch('value', { waitUntilFirstUpdate: true })
|
||||
async handleValueChange() {
|
||||
await this.updateComplete;
|
||||
this.invalid = !this.checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
/** Sets focus on the input. */
|
||||
@@ -378,7 +376,7 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
|
||||
setCustomValidity(message: string) {
|
||||
this.input.setCustomValidity(message);
|
||||
this.invalid = !this.checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -459,7 +457,6 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
enterkeyhint=${ifDefined(this.enterkeyhint)}
|
||||
inputmode=${ifDefined(this.inputmode)}
|
||||
aria-describedby="help-text"
|
||||
aria-invalid=${this.invalid ? 'true' : 'false'}
|
||||
@change=${this.handleChange}
|
||||
@input=${this.handleInput}
|
||||
@invalid=${this.handleInvalid}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { FormSubmitController } from '../../internal/form';
|
||||
import { FormControlController } from '../../internal/form';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { watch } from '../../internal/watch';
|
||||
@@ -38,7 +38,7 @@ import type { CSSResultGroup } from 'lit';
|
||||
export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
protected readonly formSubmitController = new FormSubmitController(this, {
|
||||
protected readonly formControlController = new FormControlController(this, {
|
||||
defaultValue: control => control.defaultValue
|
||||
});
|
||||
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||
@@ -50,7 +50,6 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
||||
@state() private errorMessage = '';
|
||||
@state() private customErrorMessage = '';
|
||||
@state() defaultValue = '';
|
||||
@state() invalid = false;
|
||||
|
||||
/**
|
||||
* The radio group's label. Required for proper accessibility. If you need to display HTML, use the `label` slot
|
||||
@@ -76,7 +75,7 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.invalid = !this.validity.valid;
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
private getAllRadios() {
|
||||
@@ -191,7 +190,7 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
||||
private updateCheckedRadio() {
|
||||
const radios = this.getAllRadios();
|
||||
radios.forEach(radio => (radio.checked = radio.value === this.value));
|
||||
this.invalid = !this.validity.valid;
|
||||
this.formControlController.setValidity(this.validity.valid);
|
||||
}
|
||||
|
||||
@watch('value')
|
||||
@@ -212,9 +211,9 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
||||
this.errorMessage = message;
|
||||
|
||||
if (!message) {
|
||||
this.invalid = false;
|
||||
this.formControlController.setValidity(true);
|
||||
} else {
|
||||
this.invalid = true;
|
||||
this.formControlController.setValidity(false);
|
||||
this.input.setCustomValidity(message);
|
||||
}
|
||||
}
|
||||
@@ -243,13 +242,13 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
||||
const validity = this.validity;
|
||||
|
||||
this.errorMessage = this.customErrorMessage || validity.valid ? '' : this.input.validationMessage;
|
||||
this.invalid = !validity.valid;
|
||||
this.formControlController.setValidity(validity.valid);
|
||||
|
||||
if (!validity.valid) {
|
||||
this.showNativeErrorMessage();
|
||||
}
|
||||
|
||||
return !this.invalid;
|
||||
return validity.valid;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 { FormSubmitController } from '../../internal/form';
|
||||
import { FormControlController } from '../../internal/form';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { watch } from '../../internal/watch';
|
||||
@@ -46,8 +46,7 @@ import type { CSSResultGroup } from 'lit';
|
||||
export default class SlRange extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
// @ts-expect-error - Controller is currently unused
|
||||
private readonly formSubmitController = new FormSubmitController(this);
|
||||
private readonly formControlController = new FormControlController(this);
|
||||
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private resizeObserver: ResizeObserver;
|
||||
@@ -57,7 +56,6 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@state() private hasTooltip = false;
|
||||
@state() invalid = false;
|
||||
@property() title = ''; // make reactive to pass through
|
||||
|
||||
/** The name of the range, submitted as a name/value pair with form data. */
|
||||
@@ -175,7 +173,7 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont
|
||||
|
||||
@watch('value', { waitUntilFirstUpdate: true })
|
||||
handleValueChange() {
|
||||
this.invalid = !this.checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
|
||||
// The value may have constraints, so we set the native control's value and sync it back to ensure it adhere's to
|
||||
// min, max, and step properly
|
||||
@@ -187,9 +185,8 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
||||
this.input.disabled = this.disabled;
|
||||
this.invalid = !this.checkValidity();
|
||||
// Disabled form controls are always valid
|
||||
this.formControlController.setValidity(this.disabled);
|
||||
}
|
||||
|
||||
@watch('hasTooltip', { waitUntilFirstUpdate: true })
|
||||
@@ -242,7 +239,7 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont
|
||||
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
|
||||
setCustomValidity(message: string) {
|
||||
this.input.setCustomValidity(message);
|
||||
this.invalid = !this.checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { scrollIntoView } from 'src/internal/scroll';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { defaultValue } from '../../internal/default-value';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { FormSubmitController } from '../../internal/form';
|
||||
import { FormControlController } from '../../internal/form';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { watch } from '../../internal/watch';
|
||||
@@ -64,8 +64,7 @@ import type { CSSResultGroup } from 'lit';
|
||||
export default class SlSelect extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
// @ts-expect-error - Controller is currently unused
|
||||
private readonly formSubmitController = new FormSubmitController(this);
|
||||
private readonly formControlController = new FormControlController(this);
|
||||
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private typeToSelectString = '';
|
||||
@@ -81,7 +80,6 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
@state() displayLabel = '';
|
||||
@state() currentOption: SlOption;
|
||||
@state() selectedOptions: SlOption[] = [];
|
||||
@state() invalid = false;
|
||||
|
||||
/** The name of the select, submitted as a name/value pair with form data. */
|
||||
@property() name = '';
|
||||
@@ -512,7 +510,9 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
}
|
||||
|
||||
// Update validity
|
||||
this.updateComplete.then(() => (this.invalid = !this.checkValidity()));
|
||||
this.updateComplete.then(() => {
|
||||
this.formControlController.updateValidity();
|
||||
});
|
||||
}
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
@@ -611,7 +611,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
|
||||
setCustomValidity(message: string) {
|
||||
this.valueInput.setCustomValidity(message);
|
||||
this.invalid = !this.valueInput.checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
/** Sets focus on the control. */
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 { FormSubmitController } from '../../internal/form';
|
||||
import { FormControlController } from '../../internal/form';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { watch } from '../../internal/watch';
|
||||
import styles from './switch.styles';
|
||||
@@ -37,8 +37,7 @@ import type { CSSResultGroup } from 'lit';
|
||||
export default class SlSwitch extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
// @ts-expect-error - Controller is currently unused
|
||||
private readonly formSubmitController = new FormSubmitController(this, {
|
||||
private readonly formControlController = new FormControlController(this, {
|
||||
value: (control: SlSwitch) => (control.checked ? control.value || 'on' : undefined),
|
||||
defaultValue: (control: SlSwitch) => control.defaultChecked,
|
||||
setValue: (control: SlSwitch, checked: boolean) => (control.checked = checked)
|
||||
@@ -47,7 +46,6 @@ export default class SlSwitch extends ShoelaceElement implements ShoelaceFormCon
|
||||
@query('input[type="checkbox"]') input: HTMLInputElement;
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@state() invalid = false;
|
||||
@property() title = ''; // make reactive to pass through
|
||||
|
||||
/** The name of the switch, submitted as a name/value pair with form data. */
|
||||
@@ -72,7 +70,7 @@ export default class SlSwitch extends ShoelaceElement implements ShoelaceFormCon
|
||||
@defaultValue('checked') defaultChecked = false;
|
||||
|
||||
firstUpdated() {
|
||||
this.invalid = !this.checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
private handleBlur() {
|
||||
@@ -113,14 +111,13 @@ export default class SlSwitch extends ShoelaceElement implements ShoelaceFormCon
|
||||
@watch('checked', { waitUntilFirstUpdate: true })
|
||||
handleCheckedChange() {
|
||||
this.input.checked = this.checked; // force a sync update
|
||||
this.invalid = !this.checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
||||
this.input.disabled = this.disabled;
|
||||
this.invalid = !this.checkValidity();
|
||||
// Disabled form controls are always valid
|
||||
this.formControlController.setValidity(true);
|
||||
}
|
||||
|
||||
/** Simulates a click on the switch. */
|
||||
@@ -151,7 +148,7 @@ export default class SlSwitch extends ShoelaceElement implements ShoelaceFormCon
|
||||
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
|
||||
setCustomValidity(message: string) {
|
||||
this.input.setCustomValidity(message);
|
||||
this.invalid = !this.checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 { FormSubmitController } from '../../internal/form';
|
||||
import { FormControlController } from '../../internal/form';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { watch } from '../../internal/watch';
|
||||
@@ -37,15 +37,13 @@ import type { CSSResultGroup } from 'lit';
|
||||
export default class SlTextarea extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
// @ts-expect-error - Controller is currently unused
|
||||
private readonly formSubmitController = new FormSubmitController(this);
|
||||
private readonly formControlController = new FormControlController(this);
|
||||
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||
private resizeObserver: ResizeObserver;
|
||||
|
||||
@query('.textarea__control') input: HTMLTextAreaElement;
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@state() invalid = false;
|
||||
@property() title = ''; // make reactive to pass through
|
||||
|
||||
/** The name of the textarea, submitted as a name/value pair with form data. */
|
||||
@@ -139,7 +137,7 @@ export default class SlTextarea extends ShoelaceElement implements ShoelaceFormC
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.invalid = !this.checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@@ -179,9 +177,8 @@ export default class SlTextarea extends ShoelaceElement implements ShoelaceFormC
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
||||
this.input.disabled = this.disabled;
|
||||
this.invalid = !this.checkValidity();
|
||||
// Disabled form controls are always valid
|
||||
this.formControlController.setValidity(this.disabled);
|
||||
}
|
||||
|
||||
@watch('rows', { waitUntilFirstUpdate: true })
|
||||
@@ -192,7 +189,7 @@ export default class SlTextarea extends ShoelaceElement implements ShoelaceFormC
|
||||
@watch('value', { waitUntilFirstUpdate: true })
|
||||
async handleValueChange() {
|
||||
await this.updateComplete;
|
||||
this.invalid = !this.checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
this.setTextareaHeight();
|
||||
}
|
||||
|
||||
@@ -267,7 +264,7 @@ export default class SlTextarea extends ShoelaceElement implements ShoelaceFormC
|
||||
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
|
||||
setCustomValidity(message: string) {
|
||||
this.input.setCustomValidity(message);
|
||||
this.invalid = !this.checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -21,7 +21,7 @@ const userInteractedControls: WeakMap<ShoelaceFormControl, boolean> = new WeakMa
|
||||
//
|
||||
const reportValidityOverloads: WeakMap<HTMLFormElement, () => boolean> = new WeakMap();
|
||||
|
||||
export interface FormSubmitControllerOptions {
|
||||
export interface FormControlControllerOptions {
|
||||
/** A function that returns the form containing the form control. */
|
||||
form: (input: ShoelaceFormControl) => HTMLFormElement | null;
|
||||
/** A function that returns the form control's name, which will be submitted with the form data. */
|
||||
@@ -41,12 +41,12 @@ export interface FormSubmitControllerOptions {
|
||||
setValue: (input: ShoelaceFormControl, value: unknown) => void;
|
||||
}
|
||||
|
||||
export class FormSubmitController implements ReactiveController {
|
||||
export class FormControlController implements ReactiveController {
|
||||
host: ShoelaceFormControl & ReactiveControllerHost;
|
||||
form?: HTMLFormElement | null;
|
||||
options: FormSubmitControllerOptions;
|
||||
options: FormControlControllerOptions;
|
||||
|
||||
constructor(host: ReactiveControllerHost & ShoelaceFormControl, options?: Partial<FormSubmitControllerOptions>) {
|
||||
constructor(host: ReactiveControllerHost & ShoelaceFormControl, options?: Partial<FormControlControllerOptions>) {
|
||||
(this.host = host).addController(this);
|
||||
this.options = {
|
||||
form: input => input.closest('form'),
|
||||
@@ -112,37 +112,12 @@ export class FormSubmitController implements ReactiveController {
|
||||
}
|
||||
|
||||
hostUpdated() {
|
||||
//
|
||||
// We're mapping the following "states" to data attributes. In the future, we can use ElementInternals.states to
|
||||
// create a similar mapping, but instead of [data-invalid] it will look like :--invalid.
|
||||
//
|
||||
// See this RFC for more details: https://github.com/shoelace-style/shoelace/issues/1011
|
||||
//
|
||||
const host = this.host;
|
||||
const hasInteracted = Boolean(userInteractedControls.get(host));
|
||||
const invalid = Boolean(host.invalid);
|
||||
const required = Boolean(host.required);
|
||||
|
||||
if (this.form?.noValidate) {
|
||||
// Form validation is disabled, remove the attributes
|
||||
host.removeAttribute('data-required');
|
||||
host.removeAttribute('data-optional');
|
||||
host.removeAttribute('data-invalid');
|
||||
host.removeAttribute('data-valid');
|
||||
host.removeAttribute('data-user-invalid');
|
||||
host.removeAttribute('data-user-valid');
|
||||
} else {
|
||||
// Form validation is enabled, set the attributes
|
||||
host.toggleAttribute('data-required', required);
|
||||
host.toggleAttribute('data-optional', !required);
|
||||
host.toggleAttribute('data-invalid', invalid);
|
||||
host.toggleAttribute('data-valid', !invalid);
|
||||
host.toggleAttribute('data-user-invalid', invalid && hasInteracted);
|
||||
host.toggleAttribute('data-user-valid', !invalid && hasInteracted);
|
||||
if (this.host.hasUpdated) {
|
||||
this.setValidity(this.host.checkValidity());
|
||||
}
|
||||
}
|
||||
|
||||
handleFormData(event: FormDataEvent) {
|
||||
private handleFormData(event: FormDataEvent) {
|
||||
const disabled = this.options.disabled(this.host);
|
||||
const name = this.options.name(this.host);
|
||||
const value = this.options.value(this.host);
|
||||
@@ -162,7 +137,7 @@ export class FormSubmitController implements ReactiveController {
|
||||
}
|
||||
}
|
||||
|
||||
handleFormSubmit(event: Event) {
|
||||
private handleFormSubmit(event: Event) {
|
||||
const disabled = this.options.disabled(this.host);
|
||||
const reportValidity = this.options.reportValidity;
|
||||
|
||||
@@ -179,17 +154,17 @@ export class FormSubmitController implements ReactiveController {
|
||||
}
|
||||
}
|
||||
|
||||
handleFormReset() {
|
||||
private handleFormReset() {
|
||||
this.options.setValue(this.host, this.options.defaultValue(this.host));
|
||||
this.setUserInteracted(this.host, false);
|
||||
}
|
||||
|
||||
async handleUserInput() {
|
||||
private async handleUserInput() {
|
||||
await this.host.updateComplete;
|
||||
this.setUserInteracted(this.host, true);
|
||||
}
|
||||
|
||||
reportFormValidity() {
|
||||
private reportFormValidity() {
|
||||
//
|
||||
// Shoelace form controls work hard to act like regular form controls. They support the Constraint Validation API
|
||||
// and its associated methods such as setCustomValidity() and reportValidity(). However, the HTMLFormElement also
|
||||
@@ -219,12 +194,12 @@ export class FormSubmitController implements ReactiveController {
|
||||
return true;
|
||||
}
|
||||
|
||||
setUserInteracted(el: ShoelaceFormControl, hasInteracted: boolean) {
|
||||
private setUserInteracted(el: ShoelaceFormControl, hasInteracted: boolean) {
|
||||
userInteractedControls.set(el, hasInteracted);
|
||||
el.requestUpdate();
|
||||
}
|
||||
|
||||
doAction(type: 'submit' | 'reset', invoker?: HTMLInputElement | SlButton) {
|
||||
private doAction(type: 'submit' | 'reset', invoker?: HTMLInputElement | SlButton) {
|
||||
if (this.form) {
|
||||
const button = document.createElement('button');
|
||||
button.type = type;
|
||||
@@ -264,4 +239,47 @@ export class FormSubmitController implements ReactiveController {
|
||||
// native submit button into the form, "click" it, then remove it to simulate a standard form submission.
|
||||
this.doAction('submit', invoker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously sets the form control's validity. Call this when you know the future validity but need to update
|
||||
* the host element immediately, i.e. before Lit updates the component in the next update.
|
||||
*/
|
||||
setValidity(isValid: boolean) {
|
||||
const host = this.host;
|
||||
const hasInteracted = Boolean(userInteractedControls.get(host));
|
||||
const required = Boolean(host.required);
|
||||
|
||||
//
|
||||
// We're mapping the following "states" to data attributes. In the future, we can use ElementInternals.states to
|
||||
// create a similar mapping, but instead of [data-invalid] it will look like :--invalid.
|
||||
//
|
||||
// See this RFC for more details: https://github.com/shoelace-style/shoelace/issues/1011
|
||||
//
|
||||
if (this.form?.noValidate) {
|
||||
// Form validation is disabled, remove the attributes
|
||||
host.removeAttribute('data-required');
|
||||
host.removeAttribute('data-optional');
|
||||
host.removeAttribute('data-invalid');
|
||||
host.removeAttribute('data-valid');
|
||||
host.removeAttribute('data-user-invalid');
|
||||
host.removeAttribute('data-user-valid');
|
||||
} else {
|
||||
// Form validation is enabled, set the attributes
|
||||
host.toggleAttribute('data-required', required);
|
||||
host.toggleAttribute('data-optional', !required);
|
||||
host.toggleAttribute('data-invalid', !isValid);
|
||||
host.toggleAttribute('data-valid', isValid);
|
||||
host.toggleAttribute('data-user-invalid', !isValid && hasInteracted);
|
||||
host.toggleAttribute('data-user-valid', isValid && hasInteracted);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the form control's validity based on the current value of `host.checkValidity()`. Call this when anything
|
||||
* that affects constraint validation changes so the component receives the correct validity states.
|
||||
*/
|
||||
updateValidity() {
|
||||
const host = this.host;
|
||||
this.setValidity(host.checkValidity());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,6 @@ export interface ShoelaceFormControl extends ShoelaceElement {
|
||||
minlength?: number;
|
||||
maxlength?: number;
|
||||
|
||||
// Proprietary validation properties (non-attributes)
|
||||
invalid: boolean;
|
||||
|
||||
// Validation methods
|
||||
checkValidity: () => boolean;
|
||||
reportValidity: () => boolean;
|
||||
|
||||
Reference in New Issue
Block a user