diff --git a/CHANGELOG.md b/CHANGELOG.md index fcc6d938c..92eb52fda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,6 @@ ## 2.0.0-beta.17 -- Added `required` to `sl-checkbox` and `sl-switch` - Added `minlength` and `spellcheck` attributes to `sl-textarea` - Fixed a bug where clicking a tag in `sl-select` wouldn't toggle the menu - Fixed a bug where options where `sl-select` options weren't always visible or scrollable @@ -15,7 +14,9 @@ **Form validation has been reworked and is much more powerful now!** The following changes affect `sl-input`, `sl-select`, and `sl-textarea`. - The `invalid` prop now reflects the control's validity as determined by the browser's constraint validation API -- Removed the `valid` prop +- Added `required` to `sl-checkbox`, `sl-radio`, and `sl-switch` +- Added `reportValidity()` and `setCustomValidity()` methods to all form controls +- Removed the `valid` prop from all form controls - Removed valid and invalid design tokens and related styles (you can use your own custom styles to achieve this) ## 2.0.0-beta.16 diff --git a/docs/components/form.md b/docs/components/form.md index f449d783f..aaab62939 100644 --- a/docs/components/form.md +++ b/docs/components/form.md @@ -86,8 +86,17 @@ To make a field required, use the `required` prop. The form will not be submitte
+ + Birds + Cats + Dogs + Other + +

+ Check me before submitting +

Submit
@@ -135,7 +144,7 @@ Some input types will automatically trigger constraints, such as `email` and `ur ### Custom Validation -To create a custom validation error, use the `customValidity` prop. The form will not be submitted when this prop is set to anything other than an empty string, and its value will be shown by the browser as the error message. +To create a custom validation error, use the `setCustomValidity` method. The form will not be submitted when this method is called with anything other than an empty string, and its message will be shown by the browser as the error message. ```html preview @@ -151,9 +160,9 @@ To create a custom validation error, use the `customValidity` prop. The form wil form.addEventListener('slSubmit', () => alert('All fields are valid!')); input.addEventListener('slInput', () => { if (input.value === 'shoelace') { - input.customValidity = ''; + input.setCustomValidity(''); } else { - input.customValidity = 'Hey, you\'re supposed to type \'shoelace\' before submitting this!'; + input.setCustomValidity('Hey, you\'re supposed to type \'shoelace\' before submitting this!'); } }); diff --git a/src/components.d.ts b/src/components.d.ts index 1294def4b..f59abeed0 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -217,6 +217,10 @@ export namespace Components { * Set to true to draw the checkbox in an indeterminate state. */ "indeterminate": boolean; + /** + * This will be true when the control is in an invalid state. Validity is determined by the `required` prop. + */ + "invalid": boolean; /** * The checkbox's name attribute. */ @@ -225,10 +229,18 @@ export namespace Components { * Removes focus from the checkbox. */ "removeFocus": () => Promise; + /** + * Checks for validity and shows the browser's validation message if the control is invalid. + */ + "reportValidity": () => Promise; /** * Set to true to make the checkbox a required field. */ "required": boolean; + /** + * Sets a custom validation message. If `message` is not empty, the field will be considered invalid. + */ + "setCustomValidity": (message: string) => Promise; /** * Sets focus on the checkbox. */ @@ -486,10 +498,6 @@ export namespace Components { * Set to true to add a clear button when the input is populated. */ "clearable": boolean; - /** - * Sets a custom validation message for the control. When this prop is not an empty string, the browser will assume the control is invalid and show this message as an error when the form is submitted. - */ - "customValidity": string; /** * Set to true to disable the input. */ @@ -499,7 +507,7 @@ export namespace Components { */ "inputmode": 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url'; /** - * This will be true when the control is in an invalid state. Validity is determined by props such as `type`, `required`, `pattern`, and `customValidity` using the browser's constraint validation API. + * This will be true when the control is in an invalid state. Validity is determined by props such as `type`, `required`, `minlength`, `maxlength`, and `pattern` using the browser's constraint validation API. */ "invalid": boolean; /** @@ -558,6 +566,10 @@ export namespace Components { * Selects all the text in the input. */ "select": () => Promise; + /** + * Sets a custom validation message. If `message` is not empty, the field will be considered invalid. + */ + "setCustomValidity": (message: string) => Promise; /** * Sets focus on the input. */ @@ -656,6 +668,10 @@ export namespace Components { * Set to true to disable the radio. */ "disabled": boolean; + /** + * This will be true when the control is in an invalid state. Validity in range inputs is determined by the message provided by the `setCustomValidity` method. + */ + "invalid": boolean; /** * The radio's name attribute. */ @@ -664,6 +680,14 @@ export namespace Components { * Removes focus from the radio. */ "removeFocus": () => Promise; + /** + * Checks for validity and shows the browser's validation message if the control is invalid. + */ + "reportValidity": () => Promise; + /** + * Sets a custom validation message. If `message` is not empty, the field will be considered invalid. + */ + "setCustomValidity": (message: string) => Promise; /** * Sets focus on the radio. */ @@ -678,6 +702,10 @@ export namespace Components { * Set to true to disable the input. */ "disabled": boolean; + /** + * This will be true when the control is in an invalid state. Validity in range inputs is determined by the message provided by the `setCustomValidity` method. + */ + "invalid": boolean; /** * The input's max attribute. */ @@ -694,6 +722,10 @@ export namespace Components { * Removes focus from the input. */ "removeFocus": () => Promise; + /** + * Sets a custom validation message. If `message` is not empty, the field will be considered invalid. + */ + "setCustomValidity": (message: string) => Promise; /** * Sets focus on the input. */ @@ -763,7 +795,7 @@ export namespace Components { */ "hoist": boolean; /** - * Set to true to indicate that the user input is invalid. + * This will be true when the control is in an invalid state. Validity is determined by the `required` prop. */ "invalid": boolean; /** @@ -790,10 +822,18 @@ export namespace Components { * The select's placeholder text. */ "placeholder": string; + /** + * Checks for validity and shows the browser's validation message if the control is invalid. + */ + "reportValidity": () => Promise; /** * The select's required attribute. */ "required": boolean; + /** + * Sets a custom validation message. If `message` is not empty, the field will be considered invalid. + */ + "setCustomValidity": (message: string) => Promise; /** * The select's size. */ @@ -820,6 +860,10 @@ export namespace Components { * Set to true to disable the switch. */ "disabled": boolean; + /** + * This will be true when the control is in an invalid state. Validity is determined by the `required` prop. + */ + "invalid": boolean; /** * The switch's name attribute. */ @@ -828,10 +872,18 @@ export namespace Components { * Removes focus from the switch. */ "removeFocus": () => Promise; + /** + * Checks for validity and shows the browser's validation message if the control is invalid. + */ + "reportValidity": () => Promise; /** * Set to true to make the switch a required field. */ "required": boolean; + /** + * Sets a custom validation message. If `message` is not empty, the field will be considered invalid. + */ + "setCustomValidity": (message: string) => Promise; /** * Sets focus on the switch. */ @@ -918,10 +970,6 @@ export namespace Components { * The textarea's autofocus attribute. */ "autofocus": boolean; - /** - * Sets a custom validation message for the control. When this prop is not an empty string, the browser will assume the control is invalid and show this message as an error when the form is submitted. - */ - "customValidity": string; /** * Set to true to disable the textarea. */ @@ -931,7 +979,7 @@ export namespace Components { */ "inputmode": 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url'; /** - * This will be true when the control is in an invalid state. Validity is determined by props such as `type`, `required`, `pattern`, and `customValidity` using the browser's constraint validation API. + * This will be true when the control is in an invalid state. Validity is determined by props such as `required`, `minlength`, and `maxlength` using the browser's constraint validation API. */ "invalid": boolean; /** @@ -982,6 +1030,10 @@ export namespace Components { * Selects all the text in the input. */ "select": () => Promise; + /** + * Sets a custom validation message. If `message` is not empty, the field will be considered invalid. + */ + "setCustomValidity": (message: string) => Promise; /** * Sets focus on the textarea. */ @@ -1535,6 +1587,10 @@ declare namespace LocalJSX { * Set to true to draw the checkbox in an indeterminate state. */ "indeterminate"?: boolean; + /** + * This will be true when the control is in an invalid state. Validity is determined by the `required` prop. + */ + "invalid"?: boolean; /** * The checkbox's name attribute. */ @@ -1872,10 +1928,6 @@ declare namespace LocalJSX { * Set to true to add a clear button when the input is populated. */ "clearable"?: boolean; - /** - * Sets a custom validation message for the control. When this prop is not an empty string, the browser will assume the control is invalid and show this message as an error when the form is submitted. - */ - "customValidity"?: string; /** * Set to true to disable the input. */ @@ -1885,7 +1937,7 @@ declare namespace LocalJSX { */ "inputmode"?: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url'; /** - * This will be true when the control is in an invalid state. Validity is determined by props such as `type`, `required`, `pattern`, and `customValidity` using the browser's constraint validation API. + * This will be true when the control is in an invalid state. Validity is determined by props such as `type`, `required`, `minlength`, `maxlength`, and `pattern` using the browser's constraint validation API. */ "invalid"?: boolean; /** @@ -2054,6 +2106,10 @@ declare namespace LocalJSX { * Set to true to disable the radio. */ "disabled"?: boolean; + /** + * This will be true when the control is in an invalid state. Validity in range inputs is determined by the message provided by the `setCustomValidity` method. + */ + "invalid"?: boolean; /** * The radio's name attribute. */ @@ -2080,6 +2136,10 @@ declare namespace LocalJSX { * Set to true to disable the input. */ "disabled"?: boolean; + /** + * This will be true when the control is in an invalid state. Validity in range inputs is determined by the message provided by the `setCustomValidity` method. + */ + "invalid"?: boolean; /** * The input's max attribute. */ @@ -2165,7 +2225,7 @@ declare namespace LocalJSX { */ "hoist"?: boolean; /** - * Set to true to indicate that the user input is invalid. + * This will be true when the control is in an invalid state. Validity is determined by the `required` prop. */ "invalid"?: boolean; /** @@ -2234,6 +2294,10 @@ declare namespace LocalJSX { * Set to true to disable the switch. */ "disabled"?: boolean; + /** + * This will be true when the control is in an invalid state. Validity is determined by the `required` prop. + */ + "invalid"?: boolean; /** * The switch's name attribute. */ @@ -2336,10 +2400,6 @@ declare namespace LocalJSX { * The textarea's autofocus attribute. */ "autofocus"?: boolean; - /** - * Sets a custom validation message for the control. When this prop is not an empty string, the browser will assume the control is invalid and show this message as an error when the form is submitted. - */ - "customValidity"?: string; /** * Set to true to disable the textarea. */ @@ -2349,7 +2409,7 @@ declare namespace LocalJSX { */ "inputmode"?: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url'; /** - * This will be true when the control is in an invalid state. Validity is determined by props such as `type`, `required`, `pattern`, and `customValidity` using the browser's constraint validation API. + * This will be true when the control is in an invalid state. Validity is determined by props such as `required`, `minlength`, and `maxlength` using the browser's constraint validation API. */ "invalid"?: boolean; /** diff --git a/src/components/checkbox/checkbox.tsx b/src/components/checkbox/checkbox.tsx index a1d23e12f..bd6b5f8a6 100644 --- a/src/components/checkbox/checkbox.tsx +++ b/src/components/checkbox/checkbox.tsx @@ -45,6 +45,9 @@ export class Checkbox { /** Set to true to draw the checkbox in an indeterminate state. */ @Prop({ mutable: true, reflect: true }) indeterminate = false; + /** This will be true when the control is in an invalid state. Validity is determined by the `required` prop. */ + @Prop({ mutable: true, reflect: true }) invalid = false; + /** Emitted when the control loses focus. */ @Event() slBlur: EventEmitter; @@ -85,6 +88,19 @@ export class Checkbox { this.input.blur(); } + /** Checks for validity and shows the browser's validation message if the control is invalid. */ + @Method() + async reportValidity() { + return this.input.reportValidity(); + } + + /** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */ + @Method() + async setCustomValidity(message: string) { + this.input.setCustomValidity(message); + this.invalid = !this.input.checkValidity(); + } + handleClick() { this.checked = this.input.checked; this.indeterminate = this.input.indeterminate; diff --git a/src/components/input/input.tsx b/src/components/input/input.tsx index 783be3c55..d5c8a3248 100644 --- a/src/components/input/input.tsx +++ b/src/components/input/input.tsx @@ -102,16 +102,10 @@ export class Input { /** * This will be true when the control is in an invalid state. Validity is determined by props such as `type`, - * `required`, `pattern`, and `customValidity` using the browser's constraint validation API. + * `required`, `minlength`, `maxlength`, and `pattern` using the browser's constraint validation API. */ @Prop({ mutable: true, reflect: true }) invalid = false; - /** - * Sets a custom validation message for the control. When this prop is not an empty string, the browser will assume - * the control is invalid and show this message as an error when the form is submitted. - */ - @Prop() customValidity = ''; - /** Set to true to add a clear button when the input is populated. */ @Prop() clearable = false; @@ -131,12 +125,6 @@ export class Input { this.invalid ? this.slInvalid.emit() : this.slValid.emit(); } - @Watch('customValidity') - handleCustomValidityChange() { - this.input.setCustomValidity(this.customValidity); - this.invalid = !this.input.checkValidity(); - } - /** Emitted when the control's value changes. */ @Event() slChange: EventEmitter; @@ -169,10 +157,6 @@ export class Input { this.handlePasswordToggle = this.handlePasswordToggle.bind(this); } - componentDidLoad() { - this.input.setCustomValidity(this.customValidity); - } - /** Sets focus on the input. */ @Method() async setFocus() { @@ -224,6 +208,13 @@ export class Input { return this.input.reportValidity(); } + /** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */ + @Method() + async setCustomValidity(message: string) { + this.input.setCustomValidity(message); + this.invalid = !this.input.checkValidity(); + } + handleChange() { this.value = this.input.value; this.slChange.emit(); diff --git a/src/components/radio/radio.tsx b/src/components/radio/radio.tsx index fe9a9e476..9a053d49e 100644 --- a/src/components/radio/radio.tsx +++ b/src/components/radio/radio.tsx @@ -40,6 +40,12 @@ export class Radio { /** Set to true to draw the radio in a checked state. */ @Prop({ mutable: true, reflect: true }) checked = false; + /** + * This will be true when the control is in an invalid state. Validity in range inputs is determined by the message + * provided by the `setCustomValidity` method. + */ + @Prop({ mutable: true, reflect: true }) invalid = false; + @Watch('checked') handleCheckedChange() { if (this.checked) { @@ -78,6 +84,19 @@ export class Radio { this.input.blur(); } + /** Checks for validity and shows the browser's validation message if the control is invalid. */ + @Method() + async reportValidity() { + return this.input.reportValidity(); + } + + /** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */ + @Method() + async setCustomValidity(message: string) { + this.input.setCustomValidity(message); + this.invalid = !this.input.checkValidity(); + } + getAllRadios() { const form = this.host.closest('sl-form, form') || document.body; diff --git a/src/components/range/range.tsx b/src/components/range/range.tsx index ad6841afd..c7dea11db 100644 --- a/src/components/range/range.tsx +++ b/src/components/range/range.tsx @@ -32,6 +32,12 @@ export class Range { /** Set to true to disable the input. */ @Prop() disabled = false; + /** + * This will be true when the control is in an invalid state. Validity in range inputs is determined by the message + * provided by the `setCustomValidity` method. + */ + @Prop({ mutable: true, reflect: true }) invalid = false; + /** The input's min attribute. */ @Prop() min = 0; @@ -86,6 +92,13 @@ export class Range { this.input.blur(); } + /** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */ + @Method() + async setCustomValidity(message: string) { + this.input.setCustomValidity(message); + this.invalid = !this.input.checkValidity(); + } + handleInput() { this.value = Number(this.input.value); this.slChange.emit(); diff --git a/src/components/select/select.tsx b/src/components/select/select.tsx index 241fb5e92..ea4688362 100644 --- a/src/components/select/select.tsx +++ b/src/components/select/select.tsx @@ -1,4 +1,4 @@ -import { Component, Element, Event, EventEmitter, Prop, State, Watch, h } from '@stencil/core'; +import { Component, Element, Event, EventEmitter, Method, Prop, State, Watch, h } from '@stencil/core'; import ResizeObserver from 'resize-observer-polyfill'; import { getTextContent } from '../../utilities/slot'; @@ -86,7 +86,7 @@ export class Select { /** Set to true to add a clear button when the select is populated. */ @Prop() clearable = false; - /** Set to true to indicate that the user input is invalid. */ + /** This will be true when the control is in an invalid state. Validity is determined by the `required` prop. */ @Prop({ mutable: true }) invalid = false; @Watch('multiple') @@ -134,6 +134,19 @@ export class Select { requestAnimationFrame(() => this.syncItemsFromValue()); } + /** Checks for validity and shows the browser's validation message if the control is invalid. */ + @Method() + async reportValidity() { + return this.input.reportValidity(); + } + + /** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */ + @Method() + async setCustomValidity(message: string) { + // this.input.setCustomValidity(message); + // this.invalid = !this.input.checkValidity(); + } + getItemLabel(item: HTMLSlMenuItemElement) { const slot = item.shadowRoot.querySelector('slot:not([name])') as HTMLSlotElement; return getTextContent(slot); diff --git a/src/components/switch/switch.tsx b/src/components/switch/switch.tsx index fbf440547..f76009187 100644 --- a/src/components/switch/switch.tsx +++ b/src/components/switch/switch.tsx @@ -41,6 +41,9 @@ export class Switch { /** Set to true to draw the switch in a checked state. */ @Prop({ mutable: true, reflect: true }) checked = false; + /** This will be true when the control is in an invalid state. Validity is determined by the `required` prop. */ + @Prop({ mutable: true, reflect: true }) invalid = false; + @Watch('checked') handleCheckedChange() { this.input.checked = this.checked; @@ -76,6 +79,19 @@ export class Switch { this.input.blur(); } + /** Checks for validity and shows the browser's validation message if the control is invalid. */ + @Method() + async reportValidity() { + return this.input.reportValidity(); + } + + /** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */ + @Method() + async setCustomValidity(message: string) { + this.input.setCustomValidity(message); + this.invalid = !this.input.checkValidity(); + } + handleClick() { this.checked = this.input.checked; } diff --git a/src/components/textarea/textarea.tsx b/src/components/textarea/textarea.tsx index cf6e8cee0..a46efa923 100644 --- a/src/components/textarea/textarea.tsx +++ b/src/components/textarea/textarea.tsx @@ -67,17 +67,11 @@ export class Textarea { @Prop({ reflect: true }) required: boolean; /** - * This will be true when the control is in an invalid state. Validity is determined by props such as `type`, - * `required`, `pattern`, and `customValidity` using the browser's constraint validation API. + * This will be true when the control is in an invalid state. Validity is determined by props such as `required`, + * `minlength`, and `maxlength` using the browser's constraint validation API. */ @Prop({ mutable: true, reflect: true }) invalid = false; - /** - * Sets a custom validation message for the control. When this prop is not an empty string, the browser will assume - * the control is invalid and show this message as an error when the form is submitted. - */ - @Prop() customValidity = ''; - /** The textarea's autocaptialize attribute. */ @Prop() autocapitalize: string; @@ -123,12 +117,6 @@ export class Textarea { this.invalid ? this.slInvalid.emit() : this.slValid.emit(); } - @Watch('customValidity') - handleCustomValidityChange() { - this.textarea.setCustomValidity(this.customValidity); - this.invalid = !this.textarea.checkValidity(); - } - /** Emitted when the value changes and the control is valid. */ @Event() slValid: EventEmitter; @@ -143,7 +131,6 @@ export class Textarea { } componentDidLoad() { - this.textarea.setCustomValidity(this.customValidity); this.setTextareaHeight(); this.resizeObserver = new ResizeObserver(() => this.setTextareaHeight()); this.resizeObserver.observe(this.textarea); @@ -205,6 +192,13 @@ export class Textarea { return this.textarea.reportValidity(); } + /** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */ + @Method() + async setCustomValidity(message: string) { + this.textarea.setCustomValidity(message); + this.invalid = !this.textarea.checkValidity(); + } + handleChange() { this.slChange.emit(); }