kopia lustrzana https://github.com/shoelace-style/shoelace
Merge branch 'input-validation' into next
commit
9ad226924c
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -2,6 +2,7 @@
|
|||
|
||||
## 2.0.0-beta.17
|
||||
|
||||
- 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 +11,16 @@
|
|||
- 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 `invalid` prop now reflects the control's validity as determined by the browser's constraint validation API
|
||||
- Added `required` to `sl-checkbox`, `sl-select`, and `sl-switch`
|
||||
- Added `reportValidity()` and `setCustomValidity()` methods to all form controls
|
||||
- Added validation checking for custom and native form controls to `sl-form`
|
||||
- Added `novalidate` prop to `sl-form` to disable validation
|
||||
- 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
|
||||
|
||||
- Add `hoist` prop to `sl-color-picker`, `sl-dropdown`, and `sl-select` to work around panel clipping
|
||||
|
|
|
@ -188,3 +188,12 @@ When a component relies on the presence of a slot to do something, don't assume
|
|||
- Don't conditionally render any slots — always use `hidden` or `display: none` so the slot exists in the DOM
|
||||
|
||||
See the source of card, dialog, or drawer for examples.
|
||||
|
||||
### Form Controls
|
||||
|
||||
All form controls that can be validated should:
|
||||
|
||||
- Have an `invalid` prop that reflects its validity
|
||||
- Have a `setCustomValidity()` method so the user can set a custom validation message
|
||||
- Have a `reportValidity()` method that reports its validity for form submission
|
||||
- Adhere to native attributes such as `required`, `pattern`, `minlength`, `maxlength`, etc. when it makes sense
|
||||
|
|
|
@ -4,9 +4,11 @@
|
|||
|
||||
Forms collect data that can easily be processed and sent to a server.
|
||||
|
||||
All of Shoelace's components make use of the [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) to encapsulate markup, styles, and behavior. One caveat of this approach is that native `<form>` elements don't recognize Shoelace form controls.
|
||||
All Shoelace's components make use of a [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) to encapsulate markup, styles, and behavior. One caveat of this approach is that native `<form>` elements will not recognize Shoelace form controls.
|
||||
|
||||
This component solves that problem by serializing _both_ Shoelace form controls and native form controls. The resulting form data is exposed in the `slSubmit` event in a [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object.
|
||||
This component solves that problem by serializing _both_ Shoelace form controls and native form controls when the form is submitted. The resulting form data is exposed in the `slSubmit` event as a [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object.
|
||||
|
||||
Shoelace forms don't make use of `action` and `method` attributes and they don't submit the same was as native forms. To handle submission, you need to listen for the `slSubmit` event as shown in the example below and make an XHR request with the resulting form data.
|
||||
|
||||
```html preview
|
||||
<sl-form class="form-overview">
|
||||
|
@ -28,6 +30,7 @@ This component solves that problem by serializing _both_ Shoelace form controls
|
|||
<script>
|
||||
const form = document.querySelector('.form-overview');
|
||||
|
||||
// Watch for the slSubmit event
|
||||
form.addEventListener('slSubmit', event => {
|
||||
const formData = event.detail.formData;
|
||||
let output = '';
|
||||
|
@ -64,6 +67,162 @@ 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.
|
||||
## 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`, and `maxlength`. 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. If you don't want to use cilent-side validation, you can suppress this behavior by adding `novalidate` to the `<sl-form>` element.
|
||||
|
||||
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-select label="Favorite Animal" clearable required>
|
||||
<sl-menu-item value="birds">Birds</sl-menu-item>
|
||||
<sl-menu-item value="cats">Cats</sl-menu-item>
|
||||
<sl-menu-item value="dogs">Dogs</sl-menu-item>
|
||||
<sl-menu-item value="other">Other</sl-menu-item>
|
||||
</sl-select>
|
||||
<br>
|
||||
<sl-textarea name="comment" label="Comment" required></sl-textarea>
|
||||
<br>
|
||||
<sl-checkbox required>Check me before submitting</sl-checkbox>
|
||||
<br><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 `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 validation error. To make the input valid again, call the method a second time with an empty string as the argument.
|
||||
|
||||
```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.setCustomValidity('');
|
||||
} else {
|
||||
input.setCustomValidity('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>
|
||||
```
|
||||
|
||||
### Third-party Validation
|
||||
|
||||
To opt out of the browser's built-in validation and use your own, add the `novalidate` attribute to the form. This will ignore all constraints and prevent the browser from showing its own warnings when form controls are invalid.
|
||||
|
||||
Remember that the `invalid` prop on form controls reflects validity as defined by the [constraint validation API](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation). You can set it initially, but the `invalid` prop will update as the user interacts with the form control. As such, you should not rely on it to set invalid styles using a custom validation library.
|
||||
|
||||
Instead, toggle a class or data attribute and target them in your stylesheet as shown below.
|
||||
|
||||
```html
|
||||
<sl-form novalidate>
|
||||
<sl-input class="invalid"></sl-input>
|
||||
<sl-input data-invalid></sl-input>
|
||||
</sl-form>
|
||||
|
||||
<style>
|
||||
sl-input.invalid {
|
||||
...
|
||||
}
|
||||
|
||||
sl-input[data-invalid] {
|
||||
...
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
[component-metadata:sl-form]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,6 +229,18 @@ export namespace Components {
|
|||
* Removes focus from the checkbox.
|
||||
*/
|
||||
"removeFocus": () => Promise<void>;
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* Sets a custom validation message. If `message` is not empty, the field will be considered invalid.
|
||||
*/
|
||||
"setCustomValidity": (message: string) => Promise<void>;
|
||||
/**
|
||||
* Sets focus on the checkbox.
|
||||
*/
|
||||
|
@ -405,9 +421,13 @@ export namespace Components {
|
|||
*/
|
||||
"getFormData": () => Promise<FormData>;
|
||||
/**
|
||||
* Submits the form.
|
||||
* Prevent the form from validating inputs before submitting.
|
||||
*/
|
||||
"submit": () => Promise<void>;
|
||||
"novalidate": boolean;
|
||||
/**
|
||||
* 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<boolean>;
|
||||
}
|
||||
interface SlFormatBytes {
|
||||
/**
|
||||
|
@ -491,7 +511,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`, `minlength`, `maxlength`, and `pattern` using the browser's constraint validation API.
|
||||
*/
|
||||
"invalid": boolean;
|
||||
/**
|
||||
|
@ -499,19 +519,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 +539,7 @@ export namespace Components {
|
|||
*/
|
||||
"name": string;
|
||||
/**
|
||||
* The input's pattern attribute.
|
||||
* A pattern to validate input against.
|
||||
*/
|
||||
"pattern": string;
|
||||
/**
|
||||
|
@ -531,7 +551,7 @@ export namespace Components {
|
|||
*/
|
||||
"placeholder": string;
|
||||
/**
|
||||
* Set to true for a readonly input.
|
||||
* Set to true to make the input readonly.
|
||||
*/
|
||||
"readonly": boolean;
|
||||
/**
|
||||
|
@ -539,13 +559,21 @@ 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;
|
||||
/**
|
||||
* Selects all the text in the input.
|
||||
*/
|
||||
"select": () => Promise<void>;
|
||||
/**
|
||||
* Sets a custom validation message. If `message` is not empty, the field will be considered invalid.
|
||||
*/
|
||||
"setCustomValidity": (message: string) => Promise<void>;
|
||||
/**
|
||||
* Sets focus on the input.
|
||||
*/
|
||||
|
@ -574,10 +602,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.
|
||||
*/
|
||||
|
@ -648,6 +672,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.
|
||||
*/
|
||||
|
@ -656,6 +684,14 @@ export namespace Components {
|
|||
* Removes focus from the radio.
|
||||
*/
|
||||
"removeFocus": () => Promise<void>;
|
||||
/**
|
||||
* Checks for validity and shows the browser's validation message if the control is invalid.
|
||||
*/
|
||||
"reportValidity": () => Promise<boolean>;
|
||||
/**
|
||||
* Sets a custom validation message. If `message` is not empty, the field will be considered invalid.
|
||||
*/
|
||||
"setCustomValidity": (message: string) => Promise<void>;
|
||||
/**
|
||||
* Sets focus on the radio.
|
||||
*/
|
||||
|
@ -670,6 +706,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.
|
||||
*/
|
||||
|
@ -686,6 +726,10 @@ export namespace Components {
|
|||
* Removes focus from the input.
|
||||
*/
|
||||
"removeFocus": () => Promise<void>;
|
||||
/**
|
||||
* Sets a custom validation message. If `message` is not empty, the field will be considered invalid.
|
||||
*/
|
||||
"setCustomValidity": (message: string) => Promise<void>;
|
||||
/**
|
||||
* Sets focus on the input.
|
||||
*/
|
||||
|
@ -755,7 +799,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;
|
||||
/**
|
||||
|
@ -782,18 +826,22 @@ 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<boolean>;
|
||||
/**
|
||||
* 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<void>;
|
||||
/**
|
||||
* 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`.
|
||||
*/
|
||||
|
@ -816,6 +864,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.
|
||||
*/
|
||||
|
@ -824,6 +876,18 @@ export namespace Components {
|
|||
* Removes focus from the switch.
|
||||
*/
|
||||
"removeFocus": () => Promise<void>;
|
||||
/**
|
||||
* Checks for validity and shows the browser's validation message if the control is invalid.
|
||||
*/
|
||||
"reportValidity": () => Promise<boolean>;
|
||||
/**
|
||||
* 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<void>;
|
||||
/**
|
||||
* Sets focus on the switch.
|
||||
*/
|
||||
|
@ -919,7 +983,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 `required`, `minlength`, and `maxlength` using the browser's constraint validation API.
|
||||
*/
|
||||
"invalid": boolean;
|
||||
/**
|
||||
|
@ -927,9 +991,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 +1014,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.
|
||||
*/
|
||||
|
@ -962,6 +1034,10 @@ export namespace Components {
|
|||
* Selects all the text in the input.
|
||||
*/
|
||||
"select": () => Promise<void>;
|
||||
/**
|
||||
* Sets a custom validation message. If `message` is not empty, the field will be considered invalid.
|
||||
*/
|
||||
"setCustomValidity": (message: string) => Promise<void>;
|
||||
/**
|
||||
* Sets focus on the textarea.
|
||||
*/
|
||||
|
@ -979,9 +1055,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.
|
||||
*/
|
||||
|
@ -1515,6 +1591,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.
|
||||
*/
|
||||
|
@ -1531,6 +1611,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.
|
||||
*/
|
||||
|
@ -1759,7 +1843,11 @@ declare namespace LocalJSX {
|
|||
}
|
||||
interface SlForm {
|
||||
/**
|
||||
* Emitted when the form is submitted.
|
||||
* Prevent the form from validating inputs before submitting.
|
||||
*/
|
||||
"novalidate"?: boolean;
|
||||
/**
|
||||
* Emitted when the form is submitted. This event will not be emitted if any form control inside of it is in an invalid state, unless the form has the `novalidate` attribute. Note that there is never a need to prevent this event, since it doen't send a GET or POST request like native forms. To "prevent" submission, use a conditional around the XHR request you use to submit the form's data with.
|
||||
*/
|
||||
"onSlSubmit"?: (event: CustomEvent<any>) => void;
|
||||
}
|
||||
|
@ -1857,7 +1945,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`, `minlength`, `maxlength`, and `pattern` using the browser's constraint validation API.
|
||||
*/
|
||||
"invalid"?: boolean;
|
||||
/**
|
||||
|
@ -1865,19 +1953,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 +1993,7 @@ declare namespace LocalJSX {
|
|||
*/
|
||||
"onSlInput"?: (event: CustomEvent<any>) => void;
|
||||
/**
|
||||
* The input's pattern attribute.
|
||||
* A pattern to validate input against.
|
||||
*/
|
||||
"pattern"?: string;
|
||||
/**
|
||||
|
@ -1917,11 +2005,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 +2028,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.
|
||||
*/
|
||||
|
@ -2022,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.
|
||||
*/
|
||||
|
@ -2048,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.
|
||||
*/
|
||||
|
@ -2133,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;
|
||||
/**
|
||||
|
@ -2180,10 +2272,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`.
|
||||
*/
|
||||
|
@ -2206,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.
|
||||
*/
|
||||
|
@ -2222,6 +2314,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.
|
||||
*/
|
||||
|
@ -2313,7 +2409,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 `required`, `minlength`, and `maxlength` using the browser's constraint validation API.
|
||||
*/
|
||||
"invalid"?: boolean;
|
||||
/**
|
||||
|
@ -2321,9 +2417,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.
|
||||
*/
|
||||
|
@ -2369,9 +2469,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.
|
||||
*/
|
||||
|
|
|
@ -35,7 +35,6 @@
|
|||
padding: 0;
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.checkbox__icon {
|
||||
|
|
|
@ -36,12 +36,18 @@ 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;
|
||||
|
||||
/** 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;
|
||||
|
||||
|
@ -82,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;
|
||||
|
@ -155,6 +174,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}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, Event, EventEmitter, Method, h } from '@stencil/core';
|
||||
import { Component, Event, EventEmitter, Method, Prop, h } from '@stencil/core';
|
||||
|
||||
interface FormControl {
|
||||
tag: string;
|
||||
|
@ -25,7 +25,15 @@ export class Form {
|
|||
form: HTMLElement;
|
||||
formControls: FormControl[];
|
||||
|
||||
/** Emitted when the form is submitted. */
|
||||
/** Prevent the form from validating inputs before submitting. */
|
||||
@Prop() novalidate = false;
|
||||
|
||||
/**
|
||||
* Emitted when the form is submitted. This event will not be emitted if any form control inside of it is in an
|
||||
* invalid state, unless the form has the `novalidate` attribute. Note that there is never a need to prevent this
|
||||
* event, since it doen't send a GET or POST request like native forms. To "prevent" submission, use a conditional
|
||||
* around the XHR request you use to submit the form's data with.
|
||||
*/
|
||||
@Event() slSubmit: EventEmitter;
|
||||
|
||||
connectedCallback() {
|
||||
|
@ -188,13 +196,29 @@ 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;
|
||||
|
||||
if (!this.novalidate) {
|
||||
for (const el of formControlsThatReport) {
|
||||
const isValid = await el.reportValidity();
|
||||
|
||||
if (!isValid) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.slSubmit.emit({ formData, formControls });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
handleClick(event: MouseEvent) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,11 @@ export class Input {
|
|||
/** The input's autofocus attribute. */
|
||||
@Prop() autofocus: boolean;
|
||||
|
||||
/** The input's pattern attribute. */
|
||||
@Prop() pattern: string;
|
||||
|
||||
/** The input's required attribute. */
|
||||
@Prop() required: boolean;
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Prop({ mutable: true, reflect: true }) invalid = false;
|
||||
|
||||
/** Set to true to add a clear button when the input is populated. */
|
||||
@Prop() clearable = false;
|
||||
|
@ -109,11 +115,10 @@ 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;
|
||||
|
||||
/** Set to true to indicate that the user input is invalid. */
|
||||
@Prop() invalid = false;
|
||||
@Watch('value')
|
||||
handleValueChange() {
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
/** Emitted when the control's value changes. */
|
||||
@Event() slChange: EventEmitter;
|
||||
|
@ -133,6 +138,7 @@ export class Input {
|
|||
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);
|
||||
|
@ -185,6 +191,19 @@ 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();
|
||||
}
|
||||
|
||||
/** 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();
|
||||
|
@ -195,6 +214,10 @@ export class Input {
|
|||
this.slInput.emit();
|
||||
}
|
||||
|
||||
handleInvalid() {
|
||||
this.invalid = true;
|
||||
}
|
||||
|
||||
handleBlur() {
|
||||
this.hasFocus = false;
|
||||
this.slBlur.emit();
|
||||
|
@ -237,7 +260,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 +270,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 +292,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 +325,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 +381,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
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -46,7 +46,6 @@
|
|||
padding: 0;
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -19,6 +19,11 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
// Hide the caret since we use a faux readonly technique to prevent user input
|
||||
&::part(input) {
|
||||
caret-color: transparent;
|
||||
}
|
||||
|
||||
span[slot='prefix'] {
|
||||
margin-left: var(--sl-spacing-xx-small);
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
@ -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;
|
||||
/** 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')
|
||||
handleMultipleChange() {
|
||||
|
@ -119,7 +116,10 @@ export class Select {
|
|||
this.handleBlur = this.handleBlur.bind(this);
|
||||
this.handleFocus = this.handleFocus.bind(this);
|
||||
this.handleClear = this.handleClear.bind(this);
|
||||
this.handleCut = this.handleCut.bind(this);
|
||||
this.handlePaste = this.handlePaste.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.handleKeyUp = this.handleKeyUp.bind(this);
|
||||
this.handleLabelClick = this.handleLabelClick.bind(this);
|
||||
this.handleTagClick = this.handleTagClick.bind(this);
|
||||
this.handleMenuKeyDown = this.handleMenuKeyDown.bind(this);
|
||||
|
@ -137,6 +137,18 @@ 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);
|
||||
}
|
||||
|
||||
getItemLabel(item: HTMLSlMenuItemElement) {
|
||||
const slot = item.shadowRoot.querySelector('slot:not([name])') as HTMLSlotElement;
|
||||
return getTextContent(slot);
|
||||
|
@ -176,6 +188,25 @@ export class Select {
|
|||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// We can't make the <sl-input> readonly since that will block the browser's validation messages, so this prevents
|
||||
// key presses from modifying the input's value by briefly making it readonly. We don't use `preventDefault()` since
|
||||
// that would block tabbing, shortcuts, etc.
|
||||
const nativeInput = this.input.shadowRoot.querySelector('[part="input"]') as HTMLInputElement;
|
||||
nativeInput.readOnly = true;
|
||||
}
|
||||
|
||||
handleKeyUp() {
|
||||
const nativeInput = this.input.shadowRoot.querySelector('[part="input"]') as HTMLInputElement;
|
||||
nativeInput.readOnly = false;
|
||||
}
|
||||
|
||||
handleCut(event: Event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
handlePaste(event: Event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
handleLabelClick() {
|
||||
|
@ -298,7 +329,10 @@ export class Select {
|
|||
);
|
||||
}
|
||||
|
||||
this.displayLabel = '';
|
||||
// With `multiple`, the input uses the display label as its value. If no selection is made, we set it to an empty
|
||||
// string. If items are selected, we use a zero-width space so `required` validation doesn't fail, but nothing is
|
||||
// drawn in the label either. This is a bit ugly, but it gets the job done.
|
||||
this.displayLabel = this.value.length === 0 ? '' : '\u200B';
|
||||
} else {
|
||||
const checkedItem = items.filter(item => item.value === value[0])[0];
|
||||
this.displayLabel = checkedItem ? this.getItemLabel(checkedItem) : '';
|
||||
|
@ -325,7 +359,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 +370,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}
|
||||
|
@ -379,9 +411,7 @@ export class Select {
|
|||
disabled={this.disabled}
|
||||
pill={this.pill}
|
||||
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}
|
||||
|
@ -391,6 +421,9 @@ export class Select {
|
|||
onSlBlur={this.handleBlur}
|
||||
onSlClear={this.handleClear}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
onCut={this.handleCut}
|
||||
onPaste={this.handlePaste}
|
||||
>
|
||||
{this.displayTags.length && (
|
||||
<span part="tags" slot="prefix" class="select__tags">
|
||||
|
@ -422,7 +455,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
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -53,7 +53,6 @@
|
|||
padding: 0;
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,9 +35,15 @@ 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;
|
||||
|
||||
/** 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;
|
||||
|
@ -73,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;
|
||||
}
|
||||
|
@ -129,6 +148,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}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,32 @@ 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 `required`,
|
||||
* `minlength`, and `maxlength` using the browser's constraint validation API.
|
||||
*/
|
||||
@Prop({ mutable: true, reflect: true }) invalid = false;
|
||||
|
||||
/** The textarea's autocaptialize attribute. */
|
||||
@Prop() autocapitalize: string;
|
||||
|
@ -69,21 +84,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 +107,11 @@ export class Textarea {
|
|||
this.setTextareaHeight();
|
||||
}
|
||||
|
||||
@Watch('value')
|
||||
handleValueChange() {
|
||||
this.invalid = !this.textarea.checkValidity();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleInput = this.handleInput.bind(this);
|
||||
|
@ -164,6 +175,19 @@ 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();
|
||||
}
|
||||
|
||||
/** 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();
|
||||
}
|
||||
|
@ -200,7 +224,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 +234,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 +254,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 +272,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 +297,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
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -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;
|
||||
|
|
Ładowanie…
Reference in New Issue