Rework input validation

pull/200/head
Cory LaViska 2020-08-28 16:14:39 -04:00
rodzic 05f02bb03b
commit 7c76fb3813
14 zmienionych plików z 397 dodań i 192 usunięć

Wyświetl plik

@ -2,6 +2,8 @@
## 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
- Fixed a bug where setting `null` on `sl-input`, `sl-textarea`, or `sl-select` would throw an error
@ -10,6 +12,12 @@
- Fixed a bug where the value wasn't updated and events weren't emitted when using `setRangeText` in `sl-input` and `sl-textarea`
- Optimized `hasSlot` utility by using a simpler selector
**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
- Removed valid and invalid design tokens and related styles (you can use your own custom styles to achieve this)
## 2.0.0-beta.16
- Add `hoist` prop to `sl-color-picker`, `sl-dropdown`, and `sl-select` to work around panel clipping

Wyświetl plik

@ -64,6 +64,130 @@ This component solves that problem by serializing _both_ Shoelace form controls
</script>
```
?> Shoelace forms don't make use of `action` and `method` attributes and they don't submit automatically like native forms. To handle submission, you need to listen for the `slSubmit` event as shown in the example above.
?> Shoelace forms don't make use of `action` and `method` attributes and they don't submit like native forms. To handle submission, you need to listen for the `slSubmit` event as shown in the example above and make an XHR request with the resulting form data.
## Form Control Validation
Client-side validation can be enabled through the browser's [constraint validations API](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation) for many form controls. You can enable it using props such as `required`, `pattern`, `minlength`, `maxlength`, and `customValidity`. As the user interacts with the form control, the `invalid` attribute will reflect its validity based on its current value and the constraints that have been defined.
When a form control is invalid, the containing form will not be submitted. Instead, the browser will show the user a relevant error message.
Form controls that support validation include [`sl-input`](/components/input), [`sl-textarea`](/components/textarea), [`sl-select`](/components/select), and [`sl-checkbox`](/components/checkbox). Not all validation props are available for every component. Refer to each component's documentation to see which validation props it supports.
Note that validity is not checked until the user interacts with the control or its containing form is submitted. This prevents required controls from being rendered as invalid right away, which can result in a poor user experience. If you need this behavior, set the `invalid` attribute initially.
!> Client-side validation can be used to improve the UX of forms, but it is not a replacement for server-side validation. **You should always validate and sanitize user input on the server!**
### Required Fields
To make a field required, use the `required` prop. The form will not be submitted if a required form control is empty.
```html preview
<sl-form class="input-validation-required">
<sl-input name="name" label="Name" required></sl-input>
<br>
<sl-textarea name="comment" label="Comment" required></sl-textarea>
<br>
<sl-button type="primary" submit>Submit</sl-button>
</sl-form>
<script>
const form = document.querySelector('.input-validation-required');
form.addEventListener('slSubmit', () => alert('All fields are valid!'));
</script>
```
### Input Patterns
To restrict a value to a specific [pattern](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/pattern), use the `pattern` attribute. This example only allows the letters A-Z, so the form will not submit if a number or symbol is entered. This only works with `<sl-input>` elements.
```html preview
<sl-form class="input-validation-pattern">
<sl-input name="letters" required label="Letters" pattern="[A-Za-z]+"></sl-input>
<br>
<sl-button type="primary" submit>Submit</sl-button>
</sl-form>
<script>
const form = document.querySelector('.input-validation-pattern');
form.addEventListener('slSubmit', () => alert('All fields are valid!'));
</script>
```
### Input Types
Some input types will automatically trigger constraints, such as `email` and `url`.
```html preview
<sl-form class="input-validation-type">
<sl-input type="email" label="Email" placeholder="you@example.com" required></sl-input>
<br>
<sl-input type="url" label="URL" placeholder="https://example.com/" required></sl-input>
<br>
<sl-button type="primary" submit>Submit</sl-button>
</sl-form>
<script>
const form = document.querySelector('.input-validation-type');
form.addEventListener('slSubmit', () => alert('All fields are valid!'));
</script>
```
### 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.
```html preview
<sl-form class="input-validation-custom">
<sl-input label="Type 'shoelace'" required></sl-input>
<br>
<sl-button type="primary" submit>Submit</sl-button>
</sl-form>
<script>
const form = document.querySelector('.input-validation-custom');
const input = form.querySelector('sl-input');
form.addEventListener('slSubmit', () => alert('All fields are valid!'));
input.addEventListener('slInput', () => {
if (input.value === 'shoelace') {
input.customValidity = '';
} else {
input.customValidity = 'Hey, you\'re supposed to type \'shoelace\' before submitting this!';
}
});
</script>
```
### Custom Validation Styles
The `invalid` attribute reflects the form control's validity, so you can style invalid fields using the `[invalid]` selector. The example below demonstrates how you can give erroneous fields a different appearance. Type something other than "shoelace" to demonstrate this.
```html preview
<sl-input class="custom-input" required pattern="shoelace">
<small slot="help-text">Please enter "shoelace" to continue</small>
</sl-input>
<style>
.custom-input[invalid]:not([disabled])::part(label),
.custom-input[invalid]:not([disabled])::part(help-text) {
color: var(--sl-color-danger-40);
}
.custom-input[invalid]:not([disabled])::part(base) {
border-color: var(--sl-color-danger-50);
}
.custom-input[invalid] {
--focus-ring: 0 0 0 var(--sl-focus-ring-width)
hsla(
var(--sl-color-danger-hue),
var(--sl-color-danger-saturation),
var(--sl-focus-ring-lightness),
var(--sl-focus-ring-alpha)
);
}
</style>
```
[component-metadata:sl-form]

Wyświetl plik

@ -10,6 +10,8 @@ Inputs collect data from the user.
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form.md) instead.
?> Please refer to the section on [form control validation](/components/form?id=form-control-validation) to learn how to do client-side validation.
## Examples
### Placeholders
@ -117,20 +119,4 @@ Add descriptive help text to an input with the `help-text` slot.
</sl-input>
```
### Validation
Show a valid or invalid state by setting the `valid` and `invalid` attributes, respectively. Help text can be used to provide feedback for validation and will be styled accordingly.
```html preview
<sl-input label="Valid" valid>
<div slot="help-text">This is a valid input</div>
</sl-input>
<br>
<sl-input label="Invalid" invalid>
<div slot="help-text">This is an invalid input</div>
</sl-input>
```
[component-metadata:sl-input]

Wyświetl plik

@ -10,6 +10,8 @@ Textareas collect data from the user and allow multiple lines of text.
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form.md) instead.
?> Please refer to the section on [form control validation](/components/form?id=form-control-validation) to learn how to do client-side validation.
## Examples
### Rows

132
src/components.d.ts vendored
Wyświetl plik

@ -225,6 +225,10 @@ export namespace Components {
* Removes focus from the checkbox.
*/
"removeFocus": () => Promise<void>;
/**
* Set to true to make the checkbox a required field.
*/
"required": boolean;
/**
* Sets focus on the checkbox.
*/
@ -405,9 +409,9 @@ export namespace Components {
*/
"getFormData": () => Promise<FormData>;
/**
* Submits the form.
* Submits the form. If all controls are valid, the `slSubmit` event will be emitted and the promise will resolve with `true`. If any form control is invalid, the promise will resolve with `false` and no event will be emitted.
*/
"submit": () => Promise<void>;
"submit": () => Promise<boolean>;
}
interface SlFormatBytes {
/**
@ -482,6 +486,10 @@ 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.
*/
@ -491,7 +499,7 @@ export namespace Components {
*/
"inputmode": 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
/**
* 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 props such as `type`, `required`, `pattern`, and `customValidity` using the browser's constraint validation API.
*/
"invalid": boolean;
/**
@ -499,19 +507,19 @@ export namespace Components {
*/
"label": string;
/**
* The input's max attribute.
* The input's maximum value.
*/
"max": number;
/**
* The input's maxlength attribute.
* The maximum length of input that will be considered valid.
*/
"maxlength": number;
/**
* The input's min attribute.
* The input's minimum value.
*/
"min": number;
/**
* The input's minlength attribute.
* The minimum length of input that will be considered valid.
*/
"minlength": number;
/**
@ -519,7 +527,7 @@ export namespace Components {
*/
"name": string;
/**
* The input's pattern attribute.
* A pattern to validate input against.
*/
"pattern": string;
/**
@ -531,7 +539,7 @@ export namespace Components {
*/
"placeholder": string;
/**
* Set to true for a readonly input.
* Set to true to make the input readonly.
*/
"readonly": boolean;
/**
@ -539,7 +547,11 @@ export namespace Components {
*/
"removeFocus": () => Promise<void>;
/**
* The input's required attribute.
* Checks for validity and shows the browser's validation message if the control is invalid.
*/
"reportValidity": () => Promise<boolean>;
/**
* Set to true to make the checkbox a required field.
*/
"required": boolean;
/**
@ -574,10 +586,6 @@ export namespace Components {
* The input's type.
*/
"type": 'email' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'url';
/**
* Set to true to indicate that the user input is valid.
*/
"valid": boolean;
/**
* The input's value attribute.
*/
@ -790,10 +798,6 @@ export namespace Components {
* The select's size.
*/
"size": 'small' | 'medium' | 'large';
/**
* Set to true to indicate that the user input is valid.
*/
"valid": boolean;
/**
* The value of the control. This will be a string or an array depending on `multiple`.
*/
@ -824,6 +828,10 @@ export namespace Components {
* Removes focus from the switch.
*/
"removeFocus": () => Promise<void>;
/**
* Set to true to make the switch a required field.
*/
"required": boolean;
/**
* Sets focus on the switch.
*/
@ -910,6 +918,10 @@ 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.
*/
@ -919,7 +931,7 @@ export namespace Components {
*/
"inputmode": 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
/**
* 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 props such as `type`, `required`, `pattern`, and `customValidity` using the browser's constraint validation API.
*/
"invalid": boolean;
/**
@ -927,9 +939,13 @@ export namespace Components {
*/
"label": string;
/**
* The textarea's maxlength attribute.
* The maximum length of input that will be considered valid.
*/
"maxlength": number;
/**
* The minimum length of input that will be considered valid.
*/
"minlength": number;
/**
* The textarea's name attribute.
*/
@ -946,6 +962,10 @@ export namespace Components {
* Removes focus fromt the textarea.
*/
"removeFocus": () => Promise<void>;
/**
* Checks for validity and shows the browser's validation message if the control is invalid.
*/
"reportValidity": () => Promise<boolean>;
/**
* The textarea's required attribute.
*/
@ -979,9 +999,9 @@ export namespace Components {
*/
"size": 'small' | 'medium' | 'large';
/**
* Set to true to indicate that the user input is valid.
* The textarea's spellcheck attribute.
*/
"valid": boolean;
"spellcheck": boolean;
/**
* The textarea's value attribute.
*/
@ -1531,6 +1551,10 @@ declare namespace LocalJSX {
* Emitted when the control gains focus.
*/
"onSlFocus"?: (event: CustomEvent<any>) => void;
/**
* Set to true to make the checkbox a required field.
*/
"required"?: boolean;
/**
* The checkbox's value attribute.
*/
@ -1848,6 +1872,10 @@ 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.
*/
@ -1857,7 +1885,7 @@ declare namespace LocalJSX {
*/
"inputmode"?: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
/**
* 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 props such as `type`, `required`, `pattern`, and `customValidity` using the browser's constraint validation API.
*/
"invalid"?: boolean;
/**
@ -1865,19 +1893,19 @@ declare namespace LocalJSX {
*/
"label"?: string;
/**
* The input's max attribute.
* The input's maximum value.
*/
"max"?: number;
/**
* The input's maxlength attribute.
* The maximum length of input that will be considered valid.
*/
"maxlength"?: number;
/**
* The input's min attribute.
* The input's minimum value.
*/
"min"?: number;
/**
* The input's minlength attribute.
* The minimum length of input that will be considered valid.
*/
"minlength"?: number;
/**
@ -1905,7 +1933,15 @@ declare namespace LocalJSX {
*/
"onSlInput"?: (event: CustomEvent<any>) => void;
/**
* The input's pattern attribute.
* Emitted when the value changes and the control is invalid.
*/
"onSlInvalid"?: (event: CustomEvent<any>) => void;
/**
* Emitted when the value changes and the control is valid.
*/
"onSlValid"?: (event: CustomEvent<any>) => void;
/**
* A pattern to validate input against.
*/
"pattern"?: string;
/**
@ -1917,11 +1953,11 @@ declare namespace LocalJSX {
*/
"placeholder"?: string;
/**
* Set to true for a readonly input.
* Set to true to make the input readonly.
*/
"readonly"?: boolean;
/**
* The input's required attribute.
* Set to true to make the checkbox a required field.
*/
"required"?: boolean;
/**
@ -1940,10 +1976,6 @@ declare namespace LocalJSX {
* The input's type.
*/
"type"?: 'email' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'url';
/**
* Set to true to indicate that the user input is valid.
*/
"valid"?: boolean;
/**
* The input's value attribute.
*/
@ -2180,10 +2212,6 @@ declare namespace LocalJSX {
* The select's size.
*/
"size"?: 'small' | 'medium' | 'large';
/**
* Set to true to indicate that the user input is valid.
*/
"valid"?: boolean;
/**
* The value of the control. This will be a string or an array depending on `multiple`.
*/
@ -2222,6 +2250,10 @@ declare namespace LocalJSX {
* Emitted when the control gains focus.
*/
"onSlFocus"?: (event: CustomEvent<any>) => void;
/**
* Set to true to make the switch a required field.
*/
"required"?: boolean;
/**
* The switch's value attribute.
*/
@ -2304,6 +2336,10 @@ 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.
*/
@ -2313,7 +2349,7 @@ declare namespace LocalJSX {
*/
"inputmode"?: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
/**
* 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 props such as `type`, `required`, `pattern`, and `customValidity` using the browser's constraint validation API.
*/
"invalid"?: boolean;
/**
@ -2321,9 +2357,13 @@ declare namespace LocalJSX {
*/
"label"?: string;
/**
* The textarea's maxlength attribute.
* The maximum length of input that will be considered valid.
*/
"maxlength"?: number;
/**
* The minimum length of input that will be considered valid.
*/
"minlength"?: number;
/**
* The textarea's name attribute.
*/
@ -2344,6 +2384,14 @@ declare namespace LocalJSX {
* Emitted when the control receives input.
*/
"onSlInput"?: (event: CustomEvent<any>) => void;
/**
* Emitted when the value changes and the control is invalid.
*/
"onSlInvalid"?: (event: CustomEvent<any>) => void;
/**
* Emitted when the value changes and the control is valid.
*/
"onSlValid"?: (event: CustomEvent<any>) => void;
/**
* The textarea's placeholder text.
*/
@ -2369,9 +2417,9 @@ declare namespace LocalJSX {
*/
"size"?: 'small' | 'medium' | 'large';
/**
* Set to true to indicate that the user input is valid.
* The textarea's spellcheck attribute.
*/
"valid"?: boolean;
"spellcheck"?: boolean;
/**
* The textarea's value attribute.
*/

Wyświetl plik

@ -36,6 +36,9 @@ export class Checkbox {
/** Set to true to disable the checkbox. */
@Prop() disabled = false;
/** Set to true to make the checkbox a required field. */
@Prop() required = false;
/** Set to true to draw the checkbox in a checked state. */
@Prop({ mutable: true, reflect: true }) checked = false;
@ -155,6 +158,7 @@ export class Checkbox {
value={this.value}
checked={this.checked}
disabled={this.disabled}
required={this.required}
role="checkbox"
aria-checked={this.checked}
aria-labelledby={this.labelId}

Wyświetl plik

@ -188,13 +188,27 @@ export class Form {
.filter(el => tags.includes(el.tagName.toLowerCase())) as HTMLElement[];
}
/** Submits the form. */
/**
* Submits the form. If all controls are valid, the `slSubmit` event will be emitted and the promise will resolve with
* `true`. If any form control is invalid, the promise will resolve with `false` and no event will be emitted.
*/
@Method()
async submit() {
const formData = await this.getFormData();
const formControls = await this.getFormControls();
const formControlsThatReport = formControls.filter((el: any) => typeof el.reportValidity === 'function') as any;
for (const el of formControlsThatReport) {
const isValid = await el.reportValidity();
if (!isValid) {
return false;
}
}
this.slSubmit.emit({ formData, formControls });
return true;
}
handleClick(event: MouseEvent) {

Wyświetl plik

@ -2,7 +2,11 @@
@import 'form-control-label';
@import 'form-control-help-text';
/**
* @prop --focus-ring: The focus ring style to use when the control receives focus, a `box-shadow` property.
*/
:host {
--focus-ring: var(--sl-focus-ring-box-shadow);
display: block;
}
@ -35,7 +39,7 @@
&.input--focused:not(.input--disabled) {
background-color: var(--sl-input-background-color-focus);
border-color: var(--sl-input-border-color-focus);
box-shadow: var(--sl-focus-ring-box-shadow);
box-shadow: var(--focus-ring);
.input__control {
color: var(--sl-input-color-focus);
@ -56,33 +60,6 @@
}
}
}
&.input--valid:not(.input--disabled) {
border-color: var(--sl-input-border-color-valid);
.input__control {
color: var(--sl-input-color-valid);
}
&.input--focused {
box-shadow: 0 0 0 var(--sl-focus-ring-width)
hsla(var(--sl-color-success-hue), var(--sl-color-success-saturation), 50%, var(--sl-focus-ring-alpha));
border-color: var(--sl-input-border-color-valid);
}
}
&.input--invalid:not(.input--disabled) {
border-color: var(--sl-color-danger-50);
.input__control {
color: var(--sl-input-color-invalid);
}
&.input--focused {
box-shadow: 0 0 0 var(--sl-focus-ring-width)
hsla(var(--sl-color-danger-hue), var(--sl-color-danger-saturation), 50%, var(--sl-focus-ring-alpha));
}
}
}
.input__control {

Wyświetl plik

@ -1,4 +1,4 @@
import { Component, Element, Event, EventEmitter, Method, Prop, State, h } from '@stencil/core';
import { Component, Element, Event, EventEmitter, Method, Prop, State, Watch, h } from '@stencil/core';
let id = 0;
@ -16,7 +16,7 @@ let id = 0;
* @part base - The component's base wrapper.
* @part form-control - The form control that wraps the label and the input.
* @part label - The input label.
* @part input - The synthetic input container.
* @part input - The input control.
* @part prefix - The input prefix container.
* @part clear-button - The clear button.
* @part password-toggle-button - The password toggle button.
@ -41,19 +41,19 @@ export class Input {
@State() isPasswordVisible = false;
/** The input's type. */
@Prop() type: 'email' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'url' = 'text';
@Prop({ reflect: true }) type: 'email' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'url' = 'text';
/** The input's size. */
@Prop() size: 'small' | 'medium' | 'large' = 'medium';
@Prop({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
/** The input's name attribute. */
@Prop() name = '';
@Prop({ reflect: true }) name = '';
/** The input's value attribute. */
@Prop({ mutable: true }) value: string = '';
@Prop({ mutable: true, reflect: true }) value: string = '';
/** Set to true to draw a pill-style input with rounded edges. */
@Prop() pill = false;
@Prop({ reflect: true }) pill = false;
/** The input's label. */
@Prop() label = '';
@ -62,25 +62,31 @@ export class Input {
@Prop() placeholder: string;
/** Set to true to disable the input. */
@Prop() disabled = false;
@Prop({ reflect: true }) disabled = false;
/** Set to true for a readonly input. */
@Prop() readonly = false;
/** Set to true to make the input readonly. */
@Prop({ reflect: true }) readonly = false;
/** The input's minlength attribute. */
@Prop() minlength: number;
/** The minimum length of input that will be considered valid. */
@Prop({ reflect: true }) minlength: number;
/** The input's maxlength attribute. */
@Prop() maxlength: number;
/** The maximum length of input that will be considered valid. */
@Prop({ reflect: true }) maxlength: number;
/** The input's min attribute. */
@Prop() min: number;
/** The input's minimum value. */
@Prop({ reflect: true }) min: number;
/** The input's max attribute. */
@Prop() max: number;
/** The input's maximum value. */
@Prop({ reflect: true }) max: number;
/** The input's step attribute. */
@Prop() step: number;
@Prop({ reflect: true }) step: number;
/** A pattern to validate input against. */
@Prop({ reflect: true }) pattern: string;
/** Set to true to make the checkbox a required field. */
@Prop({ reflect: true }) required: boolean;
/** The input's autocaptialize attribute. */
@Prop() autocapitalize: string;
@ -94,11 +100,17 @@ export class Input {
/** The input's autofocus attribute. */
@Prop() autofocus: boolean;
/** The input's pattern attribute. */
@Prop() pattern: string;
/**
* 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.
*/
@Prop({ mutable: true, reflect: true }) invalid = false;
/** The input's required attribute. */
@Prop() required: 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.
*/
@Prop() customValidity = '';
/** Set to true to add a clear button when the input is populated. */
@Prop() clearable = false;
@ -109,11 +121,21 @@ export class Input {
/** The input's inputmode attribute. */
@Prop() inputmode: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
/** Set to true to indicate that the user input is valid. */
@Prop() valid = false;
@Watch('value')
handleValueChange() {
this.invalid = !this.input.checkValidity();
}
/** Set to true to indicate that the user input is invalid. */
@Prop() invalid = false;
@Watch('invalid')
handleInvalidChange() {
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;
@ -130,9 +152,16 @@ export class Input {
/** Emitted when the control loses focus. */
@Event() slBlur: EventEmitter;
/** Emitted when the value changes and the control is valid. */
@Event() slValid: EventEmitter;
/** Emitted when the value changes and the control is invalid. */
@Event() slInvalid: EventEmitter;
connectedCallback() {
this.handleChange = this.handleChange.bind(this);
this.handleInput = this.handleInput.bind(this);
this.handleInvalid = this.handleInvalid.bind(this);
this.handleBlur = this.handleBlur.bind(this);
this.handleFocus = this.handleFocus.bind(this);
this.handleClearClick = this.handleClearClick.bind(this);
@ -140,6 +169,10 @@ export class Input {
this.handlePasswordToggle = this.handlePasswordToggle.bind(this);
}
componentDidLoad() {
this.input.setCustomValidity(this.customValidity);
}
/** Sets focus on the input. */
@Method()
async setFocus() {
@ -185,6 +218,12 @@ export class Input {
}
}
/** Checks for validity and shows the browser's validation message if the control is invalid. */
@Method()
async reportValidity() {
return this.input.reportValidity();
}
handleChange() {
this.value = this.input.value;
this.slChange.emit();
@ -195,6 +234,10 @@ export class Input {
this.slInput.emit();
}
handleInvalid() {
this.invalid = true;
}
handleBlur() {
this.hasFocus = false;
this.slBlur.emit();
@ -237,7 +280,6 @@ export class Input {
class={{
'form-control': true,
'form-control--has-label': this.label.length > 0,
'form-control--valid': this.valid,
'form-control--invalid': this.invalid
}}
>
@ -248,7 +290,6 @@ export class Input {
'label--small': this.size === 'small',
'label--medium': this.size === 'medium',
'label--large': this.size === 'large',
'label--valid': this.valid,
'label--invalid': this.invalid
}}
htmlFor={this.inputId}
@ -271,7 +312,6 @@ export class Input {
'input--disabled': this.disabled,
'input--focused': this.hasFocus,
'input--empty': this.value?.length === 0,
'input--valid': this.valid,
'input--invalid': this.invalid
}}
onMouseDown={this.handleMouseDown}
@ -305,8 +345,10 @@ export class Input {
inputMode={this.inputmode}
aria-labelledby={this.labelId}
aria-describedby={this.helpTextId}
aria-invalid={this.invalid}
onChange={this.handleChange}
onInput={this.handleInput}
onInvalid={this.handleInvalid}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
/>
@ -359,7 +401,6 @@ export class Input {
'help-text--small': this.size === 'small',
'help-text--medium': this.size === 'medium',
'help-text--large': this.size === 'large',
'help-text--valid': this.valid,
'help-text--invalid': this.invalid
}}
>

Wyświetl plik

@ -13,10 +13,10 @@ let id = 0;
*
* @part base - The component's base wrapper.
* @part form-control - The form control that wraps the label and the input.
* @part help-text - The select help text.
* @part icon - The select icon.
* @part input - The select input.
* @part label - The input label.
* @part help-text - The select's help text.
* @part icon - The select's icon.
* @part input - The select's input control.
* @part label - The select's label.
* @part menu - The select menu, a <sl-menu> element.
* @part tag - The multiselect option, a <sl-tag> element.
* @part tags - The container in which multiselect options are rendered.
@ -81,16 +81,13 @@ export class Select {
@Prop() label = '';
/** The select's required attribute. */
@Prop() required: boolean;
@Prop() required = false;
/** 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 valid. */
@Prop() valid = false;
/** Set to true to indicate that the user input is invalid. */
@Prop() invalid = false;
@Prop({ mutable: true }) invalid = false;
@Watch('multiple')
handleMultipleChange() {
@ -325,7 +322,6 @@ export class Select {
class={{
'form-control': true,
'form-control--has-label': this.label.length > 0,
'form-control--valid': this.valid,
'form-control--invalid': this.invalid
}}
>
@ -337,7 +333,6 @@ export class Select {
'label--small': this.size === 'small',
'label--medium': this.size === 'medium',
'label--large': this.size === 'large',
'label--valid': this.valid,
'label--invalid': this.invalid
}}
htmlFor={this.inputId}
@ -381,7 +376,6 @@ export class Select {
placeholder={this.displayLabel === '' && this.displayTags.length === 0 ? this.placeholder : null}
readonly={true}
size={this.size}
valid={this.valid}
invalid={this.invalid}
clearable={this.clearable}
required={this.required}
@ -422,7 +416,6 @@ export class Select {
'help-text--small': this.size === 'small',
'help-text--medium': this.size === 'medium',
'help-text--large': this.size === 'large',
'help-text--valid': this.valid,
'help-text--invalid': this.invalid
}}
>

Wyświetl plik

@ -35,6 +35,9 @@ export class Switch {
/** Set to true to disable the switch. */
@Prop() disabled = false;
/** Set to true to make the switch a required field. */
@Prop() required = false;
/** Set to true to draw the switch in a checked state. */
@Prop({ mutable: true, reflect: true }) checked = false;
@ -129,6 +132,7 @@ export class Switch {
value={this.value}
checked={this.checked}
disabled={this.disabled}
required={this.required}
role="switch"
aria-checked={this.checked}
aria-labelledby={this.labelId}

Wyświetl plik

@ -55,33 +55,6 @@
}
}
}
&.textarea--valid:not(.textarea--disabled) {
border-color: var(--sl-input-border-color-valid);
.textarea__control {
color: var(--sl-input-color-valid);
}
&.textarea--focused {
box-shadow: 0 0 0 var(--sl-focus-ring-width)
hsla(var(--sl-color-success-hue), var(--sl-color-success-saturation), 50%, var(--sl-focus-ring-alpha));
border-color: var(--sl-input-border-color-valid);
}
}
&.textarea--invalid:not(.textarea--disabled) {
border-color: var(--sl-color-danger-50);
.textarea__control {
color: var(--sl-input-color-invalid);
}
&.textarea--focused {
box-shadow: 0 0 0 var(--sl-focus-ring-width)
hsla(var(--sl-color-danger-hue), var(--sl-color-danger-saturation), 50%, var(--sl-focus-ring-alpha));
}
}
}
.textarea__control {

Wyświetl plik

@ -31,13 +31,13 @@ export class Textarea {
@State() hasFocus = false;
/** The textarea's size. */
@Prop() size: 'small' | 'medium' | 'large' = 'medium';
@Prop({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
/** The textarea's name attribute. */
@Prop() name = '';
@Prop({ reflect: true }) name = '';
/** The textarea's value attribute. */
@Prop({ mutable: true }) value = '';
@Prop({ mutable: true, reflect: true }) value = '';
/** The textarea's label. */
@Prop() label = '';
@ -45,17 +45,38 @@ export class Textarea {
/** The textarea's placeholder text. */
@Prop() placeholder: string;
/** Set to true to disable the textarea. */
@Prop() disabled = false;
/** Set to true for a readonly textarea. */
@Prop() readonly = false;
/** The number of rows to display by default. */
@Prop() rows = 4;
/** Controls how the textarea can be resized. */
@Prop() resize: 'none' | 'vertical' | 'auto' = 'vertical';
/** The textarea's maxlength attribute. */
@Prop() maxlength: number;
/** Set to true to disable the textarea. */
@Prop({ reflect: true }) disabled = false;
/** Set to true for a readonly textarea. */
@Prop({ reflect: true }) readonly = false;
/** The minimum length of input that will be considered valid. */
@Prop({ reflect: true }) minlength: number;
/** The maximum length of input that will be considered valid. */
@Prop({ reflect: true }) maxlength: number;
/** The textarea's required attribute. */
@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.
*/
@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;
@ -69,21 +90,12 @@ export class Textarea {
/** The textarea's autofocus attribute. */
@Prop() autofocus: boolean;
/** The textarea's required attribute. */
@Prop() required: boolean;
/** The textarea's spellcheck attribute. */
@Prop() spellcheck: boolean;
/** The textarea's inputmode attribute. */
@Prop() inputmode: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
/** Set to true to indicate that the user input is valid. */
@Prop() valid = false;
/** Set to true to indicate that the user input is invalid. */
@Prop() invalid = false;
/** The number of rows to display by default. */
@Prop() rows = 4;
/** Emitted when the control's value changes. */
@Event() slChange: EventEmitter;
@ -101,6 +113,28 @@ export class Textarea {
this.setTextareaHeight();
}
@Watch('value')
handleValueChange() {
this.invalid = !this.textarea.checkValidity();
}
@Watch('invalid')
handleInvalidChange() {
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;
/** Emitted when the value changes and the control is invalid. */
@Event() slInvalid: EventEmitter;
connectedCallback() {
this.handleChange = this.handleChange.bind(this);
this.handleInput = this.handleInput.bind(this);
@ -109,6 +143,7 @@ export class Textarea {
}
componentDidLoad() {
this.textarea.setCustomValidity(this.customValidity);
this.setTextareaHeight();
this.resizeObserver = new ResizeObserver(() => this.setTextareaHeight());
this.resizeObserver.observe(this.textarea);
@ -164,6 +199,12 @@ export class Textarea {
}
}
/** Checks for validity and shows the browser's validation message if the control is invalid. */
@Method()
async reportValidity() {
return this.textarea.reportValidity();
}
handleChange() {
this.slChange.emit();
}
@ -200,7 +241,6 @@ export class Textarea {
class={{
'form-control': true,
'form-control--has-label': this.label.length > 0,
'form-control--valid': this.valid,
'form-control--invalid': this.invalid
}}
>
@ -211,7 +251,6 @@ export class Textarea {
'label--small': this.size === 'small',
'label--medium': this.size === 'medium',
'label--large': this.size === 'large',
'label--valid': this.valid,
'label--invalid': this.invalid
}}
htmlFor={this.textareaId}
@ -232,7 +271,6 @@ export class Textarea {
'textarea--disabled': this.disabled,
'textarea--focused': this.hasFocus,
'textarea--empty': this.value?.length === 0,
'textarea--valid': this.valid,
'textarea--invalid': this.invalid,
// Modifiers
@ -251,11 +289,13 @@ export class Textarea {
disabled={this.disabled}
readOnly={this.readonly}
rows={this.rows}
minLength={this.minlength}
maxLength={this.maxlength}
value={this.value}
autoCapitalize={this.autocapitalize}
autoCorrect={this.autocorrect}
autoFocus={this.autofocus}
spellcheck={this.spellcheck}
required={this.required}
inputMode={this.inputmode}
aria-labelledby={this.labelId}
@ -274,7 +314,6 @@ export class Textarea {
'help-text--small': this.size === 'small',
'help-text--medium': this.size === 'medium',
'help-text--large': this.size === 'large',
'help-text--valid': this.valid,
'help-text--invalid': this.invalid
}}
>

Wyświetl plik

@ -168,8 +168,6 @@
--sl-input-border-color-hover: var(--sl-color-gray-70);
--sl-input-border-color-focus: var(--sl-color-primary-50);
--sl-input-border-color-disabled: var(--sl-color-gray-80);
--sl-input-border-color-valid: var(--sl-color-success-50);
--sl-input-border-color-invalid: var(--sl-color-danger-50);
--sl-input-border-width: 1px;
--sl-input-border-radius-small: var(--sl-border-radius-medium);
@ -187,8 +185,6 @@
--sl-input-color-hover: var(--sl-color-gray-30);
--sl-input-color-focus: var(--sl-color-gray-30);
--sl-input-color-disabled: var(--sl-color-gray-10);
--sl-input-color-valid: var(var(--sl-color-gray-30));
--sl-input-color-invalid: var(var(--sl-color-gray-30));
--sl-input-icon-color: var(--sl-color-gray-60);
--sl-input-icon-color-hover: var(--sl-color-gray-40);
@ -207,8 +203,6 @@
--sl-input-label-font-size-large: var(--sl-font-size-large);
--sl-input-label-color: inherit;
--sl-input-label-color-valid: inherit;
--sl-input-label-color-invalid: inherit;
// Help text
--sl-input-help-text-font-size-small: var(--sl-font-size-x-small);
@ -216,8 +210,6 @@
--sl-input-help-text-font-size-large: var(--sl-font-size-medium);
--sl-input-help-text-color: var(--sl-color-gray-60);
--sl-input-help-text-color-valid: var(--sl-color-success-40);
--sl-input-help-text-color-invalid: var(--sl-color-danger-40);
// Toggles (checkboxes, radios, switches)
--sl-toggle-size: 16px;