kopia lustrzana https://github.com/shoelace-style/shoelace
revert radio fixes
rodzic
92a73f21eb
commit
1f34b63a2e
|
@ -7,8 +7,8 @@ Radios buttons allow the user to select a single option from a group using a but
|
|||
Radio buttons are designed to be used with [radio groups](/components/radio-group). When a radio button has focus, the arrow keys can be used to change the selected option just like standard radio controls.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" value="1">
|
||||
<sl-radio-button name="a" value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-group label="Select an option">
|
||||
<sl-radio-button name="a" value="1" checked>Option 1</sl-radio-button>
|
||||
<sl-radio-button name="a" value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button name="a" value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
|
@ -18,8 +18,8 @@ Radio buttons are designed to be used with [radio groups](/components/radio-grou
|
|||
import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" value="1">
|
||||
<SlRadioButton name="option" value="1">
|
||||
<SlRadioGroup label="Select an option">
|
||||
<SlRadioButton name="option" value="1" checked>
|
||||
Option 1
|
||||
</SlRadioButton>
|
||||
<SlRadioButton name="option" value="2">
|
||||
|
@ -34,13 +34,43 @@ const App = () => (
|
|||
|
||||
## Examples
|
||||
|
||||
### Checked
|
||||
|
||||
To set the initial checked state, use the `checked` attribute.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option">
|
||||
<sl-radio-button name="option" value="1" checked>Option 1</sl-radio-button>
|
||||
<sl-radio-button name="option" value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button name="option" value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option">
|
||||
<SlRadioButton name="option" value="1" checked>
|
||||
Option 1
|
||||
</SlRadioButton>
|
||||
<SlRadioButton name="option" value="2">
|
||||
Option 2
|
||||
</SlRadioButton>
|
||||
<SlRadioButton name="option" value="3">
|
||||
Option 3
|
||||
</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
);
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` attribute to disable a radio button.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" value="1">
|
||||
<sl-radio-button name="option" value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-group label="Select an option">
|
||||
<sl-radio-button name="option" value="1" checked>Option 1</sl-radio-button>
|
||||
<sl-radio-button name="option" value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button name="option" value="3" disabled>Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
|
@ -50,8 +80,8 @@ Use the `disabled` attribute to disable a radio button.
|
|||
import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" value="1">
|
||||
<SlRadioButton name="option" value="1">
|
||||
<SlRadioGroup label="Select an option">
|
||||
<SlRadioButton name="option" value="1" checked>
|
||||
Option 1
|
||||
</SlRadioButton>
|
||||
<SlRadioButton name="option" value="2">
|
||||
|
@ -69,24 +99,24 @@ const App = () => (
|
|||
Use the `size` attribute to change a radio button's size.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" value="1">
|
||||
<sl-radio-button size="small" name="option" value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-group label="Select an option">
|
||||
<sl-radio-button size="small" name="option" value="1" checked>Option 1</sl-radio-button>
|
||||
<sl-radio-button size="small" name="option" value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button size="small" name="option" value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-radio-group label="Select an option" value="1">
|
||||
<sl-radio-button size="medium" name="option" value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-group label="Select an option">
|
||||
<sl-radio-button size="medium" name="option" value="1" checked>Option 1</sl-radio-button>
|
||||
<sl-radio-button size="medium" name="option" value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button size="medium" name="option" value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-radio-group label="Select an option" value="1">
|
||||
<sl-radio-button size="large" name="option" value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-group label="Select an option">
|
||||
<sl-radio-button size="large" name="option" value="1" checked>Option 1</sl-radio-button>
|
||||
<sl-radio-button size="large" name="option" value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button size="large" name="option" value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
|
@ -96,24 +126,24 @@ Use the `size` attribute to change a radio button's size.
|
|||
import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" value="1">
|
||||
<SlRadioButton size="small" name="option" value="1">Option 1</SlRadioButton>
|
||||
<SlRadioGroup label="Select an option">
|
||||
<SlRadioButton size="small" name="option" value="1" checked>Option 1</SlRadioButton>
|
||||
<SlRadioButton size="small" name="option" value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton size="small" name="option" value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
|
||||
<br />
|
||||
|
||||
<SlRadioGroup label="Select an option" value="1">
|
||||
<SlRadioButton size="medium" name="option" value="1">Option 1</SlRadioButton>
|
||||
<SlRadioGroup label="Select an option">
|
||||
<SlRadioButton size="medium" name="option" value="1" checked>Option 1</SlRadioButton>
|
||||
<SlRadioButton size="medium" name="option" value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton size="medium" name="option" value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
|
||||
<br />
|
||||
|
||||
<SlRadioGroup label="Select an option" value="1">
|
||||
<SlRadioButton size="large" name="option" value="1">Option 1</SlRadioButton>
|
||||
<SlRadioGroup label="Select an option">
|
||||
<SlRadioButton size="large" name="option" value="1" checked>Option 1</SlRadioButton>
|
||||
<SlRadioButton size="large" name="option" value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton size="large" name="option" value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
|
@ -125,24 +155,24 @@ const App = () => (
|
|||
Use the `pill` attribute to give radio buttons rounded edges.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" value="1">
|
||||
<sl-radio-button pill size="small" name="option" value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-group label="Select an option">
|
||||
<sl-radio-button pill size="small" name="option" value="1" checked>Option 1</sl-radio-button>
|
||||
<sl-radio-button pill size="small" name="option" value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button pill size="small" name="option" value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-radio-group label="Select an option" value="1">
|
||||
<sl-radio-button pill size="medium" name="option" value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-group label="Select an option">
|
||||
<sl-radio-button pill size="medium" name="option" value="1" checked>Option 1</sl-radio-button>
|
||||
<sl-radio-button pill size="medium" name="option" value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button pill size="medium" name="option" value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-radio-group label="Select an option" value="1">
|
||||
<sl-radio-button pill size="large" name="option" value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-group label="Select an option">
|
||||
<sl-radio-button pill size="large" name="option" value="1" checked>Option 1</sl-radio-button>
|
||||
<sl-radio-button pill size="large" name="option" value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button pill size="large" name="option" value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
|
@ -152,24 +182,24 @@ Use the `pill` attribute to give radio buttons rounded edges.
|
|||
import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" value="1">
|
||||
<SlRadioButton pill size="small" name="option" value="1">Option 1</SlRadioButton>
|
||||
<SlRadioGroup label="Select an option">
|
||||
<SlRadioButton pill size="small" name="option" value="1" checked>Option 1</SlRadioButton>
|
||||
<SlRadioButton pill size="small" name="option" value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton pill size="small" name="option" value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
|
||||
<br />
|
||||
|
||||
<SlRadioGroup label="Select an option" value="1">
|
||||
<SlRadioButton pill size="medium" name="option" value="1">Option 1</SlRadioButton>
|
||||
<SlRadioGroup label="Select an option">
|
||||
<SlRadioButton pill size="medium" name="option" value="1" checked>Option 1</SlRadioButton>
|
||||
<SlRadioButton pill size="medium" name="option" value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton pill size="medium" name="option" value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
|
||||
<br />
|
||||
|
||||
<SlRadioGroup label="Select an option" value="1">
|
||||
<SlRadioButton pill size="large" name="option" value="1">Option 1</SlRadioButton>
|
||||
<SlRadioGroup label="Select an option">
|
||||
<SlRadioButton pill size="large" name="option" value="1" checked>Option 1</SlRadioButton>
|
||||
<SlRadioButton pill size="large" name="option" value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton pill size="large" name="option" value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
|
@ -181,8 +211,8 @@ const App = () => (
|
|||
Use the `prefix` and `suffix` slots to add icons.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" value="1">
|
||||
<sl-radio-button name="a" value="1">
|
||||
<sl-radio-group label="Select an option">
|
||||
<sl-radio-button name="a" value="1" checked>
|
||||
<sl-icon slot="prefix" name="archive"></sl-icon>
|
||||
Option 1
|
||||
</sl-radio-button>
|
||||
|
@ -204,8 +234,8 @@ Use the `prefix` and `suffix` slots to add icons.
|
|||
import { SlIcon, SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" value="1">
|
||||
<SlRadioButton name="a" value="1">
|
||||
<SlRadioGroup label="Select an option">
|
||||
<SlRadioButton name="a" value="1" checked>
|
||||
<SlIcon slot="prefix" name="archive" />
|
||||
Option 1
|
||||
</SlRadioButton>
|
||||
|
@ -229,7 +259,7 @@ const App = () => (
|
|||
You can omit button labels and use icons instead. Make sure to set a `label` attribute on each icon so screen readers will announce each option correctly.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" value="neutral">
|
||||
<sl-radio-group label="Select an option">
|
||||
<sl-radio-button name="a" value="angry">
|
||||
<sl-icon name="emoji-angry" label="Angry"></sl-icon>
|
||||
</sl-radio-button>
|
||||
|
@ -238,7 +268,7 @@ You can omit button labels and use icons instead. Make sure to set a `label` att
|
|||
<sl-icon name="emoji-frown" label="Sad"></sl-icon>
|
||||
</sl-radio-button>
|
||||
|
||||
<sl-radio-button name="a" value="neutral">
|
||||
<sl-radio-button name="a" value="neutral" checked>
|
||||
<sl-icon name="emoji-neutral" label="Neutral"></sl-icon>
|
||||
</sl-radio-button>
|
||||
|
||||
|
@ -256,7 +286,7 @@ You can omit button labels and use icons instead. Make sure to set a `label` att
|
|||
import { SlIcon, SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" value="neutral">
|
||||
<SlRadioGroup label="Select an option">
|
||||
<SlRadioButton name="a" value="angry">
|
||||
<SlIcon name="emoji-angry" label="Angry" />
|
||||
</SlRadioButton>
|
||||
|
@ -265,7 +295,7 @@ const App = () => (
|
|||
<SlIcon name="emoji-frown" label="Sad" />
|
||||
</SlRadioButton>
|
||||
|
||||
<SlRadioButton name="a" value="neutral">
|
||||
<SlRadioButton name="a" value="neutral" checked>
|
||||
<SlIcon name="emoji-neutral" label="Neutral" />
|
||||
</SlRadioButton>
|
||||
|
||||
|
@ -280,4 +310,77 @@ const App = () => (
|
|||
);
|
||||
```
|
||||
|
||||
### Custom Validity
|
||||
|
||||
Use the `setCustomValidity()` method to set a custom validation message. This will prevent the form from submitting and make the browser display the error message you provide. To clear the error, call this function with an empty string.
|
||||
|
||||
```html preview
|
||||
<form class="custom-validity">
|
||||
<sl-radio-group label="Select an option">
|
||||
<sl-radio-button name="a" value="1" checked>Not me</sl-radio-button>
|
||||
<sl-radio-button name="a" value="2">Me neither</sl-radio-button>
|
||||
<sl-radio-button name="a" value="3">Choose me</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
<br />
|
||||
<sl-button type="submit" variant="primary">Submit</sl-button>
|
||||
</form>
|
||||
<script>
|
||||
const form = document.querySelector('.custom-validity');
|
||||
const radioButton = form.querySelectorAll('sl-radio-button')[2];
|
||||
const errorMessage = 'You must choose this option';
|
||||
// Set initial validity as soon as the element is defined
|
||||
customElements.whenDefined('sl-radio-button').then(() => {
|
||||
radioButton.setCustomValidity(errorMessage);
|
||||
});
|
||||
// Update validity when a selection is made
|
||||
form.addEventListener('sl-change', () => {
|
||||
const isValid = radioButton.checked;
|
||||
radioButton.setCustomValidity(isValid ? '' : errorMessage);
|
||||
});
|
||||
// Handle form submit
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { SlButton, SlIcon, SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
const App = () => {
|
||||
const radio = useRef(null);
|
||||
const errorMessage = 'You must choose this option';
|
||||
function handleChange(event) {
|
||||
radio.current.setCustomValidity(radio.current.checked ? '' : errorMessage);
|
||||
}
|
||||
function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
}
|
||||
useEffect(() => {
|
||||
radio.current.setCustomValidity(errorMessage);
|
||||
}, []);
|
||||
return (
|
||||
<form class="custom-validity" onSubmit={handleSubmit}>
|
||||
<SlRadioGroup label="Select an option">
|
||||
<SlRadioButton name="a" value="1" checked onSlChange={handleChange}>
|
||||
Not me
|
||||
</SlRadioButton>
|
||||
<SlRadioButton name="a" value="2" onSlChange={handleChange}>
|
||||
Me neither
|
||||
</SlRadioButton>
|
||||
<SlRadioButton ref={radio} name="a" value="3" onSlChange={handleChange}>
|
||||
Choose me
|
||||
</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
<br />
|
||||
<SlButton type="submit" variant="primary">
|
||||
Submit
|
||||
</SlButton>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
[component-metadata:sl-radio-button]
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
Radio groups are used to group multiple [radios](/components/radio) or [radio buttons](/components/radio-button) so they function as a single form control.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" value="1">
|
||||
<sl-radio value="1">Option 1</sl-radio>
|
||||
<sl-radio value="2">Option 2</sl-radio>
|
||||
<sl-radio value="3">Option 3</sl-radio>
|
||||
<sl-radio-group label="Select an option">
|
||||
<sl-radio name="option" value="1" checked>Option 1</sl-radio>
|
||||
<sl-radio name="option" value="2">Option 2</sl-radio>
|
||||
<sl-radio name="option" value="3">Option 3</sl-radio>
|
||||
</sl-radio-group>
|
||||
```
|
||||
|
||||
|
@ -16,10 +16,16 @@ Radio groups are used to group multiple [radios](/components/radio) or [radio bu
|
|||
import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" value="1">
|
||||
<SlRadio value="1">Option 1</SlRadio>
|
||||
<SlRadio value="2">Option 2</SlRadio>
|
||||
<SlRadio value="3">Option 3</SlRadio>
|
||||
<SlRadioGroup label="Select an option">
|
||||
<SlRadio name="option" value="1" checked>
|
||||
Option 1
|
||||
</SlRadio>
|
||||
<SlRadio name="option" value="2">
|
||||
Option 2
|
||||
</SlRadio>
|
||||
<SlRadio name="option" value="3">
|
||||
Option 3
|
||||
</SlRadio>
|
||||
</SlRadioGroup>
|
||||
);
|
||||
```
|
||||
|
@ -31,8 +37,8 @@ const App = () => (
|
|||
You can show the fieldset and legend that wraps the radio group using the `fieldset` attribute. If you don't use this option, you should still provide a label so screen readers announce the control correctly.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" value="1" fieldset>
|
||||
<sl-radio name="option" value="1">Option 1</sl-radio>
|
||||
<sl-radio-group label="Select an option" fieldset>
|
||||
<sl-radio name="option" value="1" checked>Option 1</sl-radio>
|
||||
<sl-radio name="option" value="2">Option 2</sl-radio>
|
||||
<sl-radio name="option" value="3">Option 3</sl-radio>
|
||||
</sl-radio-group>
|
||||
|
@ -42,8 +48,8 @@ You can show the fieldset and legend that wraps the radio group using the `field
|
|||
import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" value="1" fieldset>
|
||||
<SlRadio name="option" value="1">
|
||||
<SlRadioGroup label="Select an option" fieldset>
|
||||
<SlRadio name="option" value="1" checked>
|
||||
Option 1
|
||||
</SlRadio>
|
||||
<SlRadio name="option" value="2">
|
||||
|
@ -61,8 +67,8 @@ const App = () => (
|
|||
[Radio buttons](/components/radio-button) offer an alternate way to display radio controls. In this case, an internal [button group](/components/button-group) is used to group the buttons into a single, cohesive control.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" value="1">
|
||||
<sl-radio-button name="option" value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-group label="Select an option">
|
||||
<sl-radio-button name="option" value="1" checked>Option 1</sl-radio-button>
|
||||
<sl-radio-button name="option" value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button name="option" value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
|
@ -72,8 +78,8 @@ const App = () => (
|
|||
import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" value="1">
|
||||
<SlRadioButton name="option" value="1">
|
||||
<SlRadioGroup label="Select an option">
|
||||
<SlRadioButton name="option" value="1" checked>
|
||||
Option 1
|
||||
</SlRadioButton>
|
||||
<SlRadioButton name="option" value="2">
|
||||
|
@ -86,141 +92,4 @@ const App = () => (
|
|||
);
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
Setting the `required` attribute to make selecting an option mandatory. If a value has not been selected, it will prevent the form from submitting and display an error message.
|
||||
|
||||
```html preview
|
||||
<form class="validation">
|
||||
<sl-radio-group label="Select an option" required>
|
||||
<sl-radio name="a" value="1">Not me</sl-radio>
|
||||
<sl-radio name="a" value="2">Me neither</sl-radio>
|
||||
<sl-radio name="a" value="3">Choose me</sl-radio>
|
||||
</sl-radio-group>
|
||||
<br />
|
||||
<sl-button type="submit" variant="primary">Submit</sl-button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
const form = document.querySelector('.validation');
|
||||
|
||||
// Handle form submit
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlIcon, SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
const App = () => {
|
||||
function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
}
|
||||
|
||||
return (
|
||||
<form class="custom-validity" onSubmit={handleSubmit} required>
|
||||
<SlRadioGroup label="Select an option" onSlChange={handleChange}>
|
||||
<SlRadio name="a" value="1">
|
||||
Not me
|
||||
</SlRadio>
|
||||
<SlRadio name="a" value="2">
|
||||
Me neither
|
||||
</SlRadio>
|
||||
<SlRadio name="a" value="3">
|
||||
Choose me
|
||||
</SlRadio>
|
||||
</SlRadioGroup>
|
||||
<br />
|
||||
<SlButton type="submit" variant="primary">
|
||||
Submit
|
||||
</SlButton>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### Custom Validity
|
||||
|
||||
Use the `setCustomValidity()` method to set a custom validation message. This will prevent the form from submitting and make the browser display the error message you provide. To clear the error, call this function with an empty string.
|
||||
|
||||
```html preview
|
||||
<form class="custom-validity">
|
||||
<sl-radio-group label="Select an option" value="1">
|
||||
<sl-radio name="a" value="1">Not me</sl-radio>
|
||||
<sl-radio name="a" value="2">Me neither</sl-radio>
|
||||
<sl-radio name="a" value="3">Choose me</sl-radio>
|
||||
</sl-radio-group>
|
||||
<br />
|
||||
<sl-button type="submit" variant="primary">Submit</sl-button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
const form = document.querySelector('.custom-validity');
|
||||
const radioGroup = form.querySelector('sl-radio-group');
|
||||
const errorMessage = 'You must choose the last option';
|
||||
|
||||
// Set initial validity as soon as the element is defined
|
||||
customElements.whenDefined('sl-radio-group').then(() => {
|
||||
radioGroup.setCustomValidity(errorMessage);
|
||||
});
|
||||
|
||||
// Update validity when a selection is made
|
||||
form.addEventListener('sl-change', () => {
|
||||
const isValid = radioGroup.value === '3';
|
||||
radioGroup.setCustomValidity(isValid ? '' : errorMessage);
|
||||
});
|
||||
|
||||
// Handle form submit
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { SlButton, SlIcon, SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
const App = () => {
|
||||
const radioGroup = useRef(null);
|
||||
const errorMessage = 'You must choose this option';
|
||||
|
||||
function handleChange() {
|
||||
radioGroup.current.setCustomValidity(radioGroup.current.value === '3' ? '' : errorMessage);
|
||||
}
|
||||
|
||||
function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
radio.current.setCustomValidity(errorMessage);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<form class="custom-validity" onSubmit={handleSubmit}>
|
||||
<SlRadioGroup ref={radioGroup} label="Select an option" value="1" onSlChange={handleChange}>
|
||||
<SlRadio name="a" value="1">
|
||||
Not me
|
||||
</SlRadio>
|
||||
<SlRadio name="a" value="2">
|
||||
Me neither
|
||||
</SlRadio>
|
||||
<SlRadio name="a" value="3">
|
||||
Choose me
|
||||
</SlRadio>
|
||||
</SlRadioGroup>
|
||||
<br />
|
||||
<SlButton type="submit" variant="primary">
|
||||
Submit
|
||||
</SlButton>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
[component-metadata:sl-radio-group]
|
||||
|
|
|
@ -7,8 +7,8 @@ Radios allow the user to select a single option from a group.
|
|||
Radios are designed to be used with [radio groups](/components/radio-group).
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" value="1">
|
||||
<sl-radio name="option" value="1">Option 1</sl-radio>
|
||||
<sl-radio-group label="Select an option">
|
||||
<sl-radio name="option" value="1" checked>Option 1</sl-radio>
|
||||
<sl-radio name="option" value="2">Option 2</sl-radio>
|
||||
<sl-radio name="option" value="3">Option 3</sl-radio>
|
||||
</sl-radio-group>
|
||||
|
@ -18,8 +18,8 @@ Radios are designed to be used with [radio groups](/components/radio-group).
|
|||
import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" value="1">
|
||||
<SlRadio name="option" value="1">
|
||||
<SlRadioGroup label="Select an option">
|
||||
<SlRadio name="option" value="1" checked>
|
||||
Option 1
|
||||
</SlRadio>
|
||||
<SlRadio name="option" value="2">
|
||||
|
@ -41,8 +41,8 @@ const App = () => (
|
|||
To set the initial checked state, use the `checked` attribute.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" value="1">
|
||||
<sl-radio name="option" value="1">Option 1</sl-radio>
|
||||
<sl-radio-group label="Select an option">
|
||||
<sl-radio name="option" value="1" checked>Option 1</sl-radio>
|
||||
<sl-radio name="option" value="2">Option 2</sl-radio>
|
||||
<sl-radio name="option" value="3">Option 3</sl-radio>
|
||||
</sl-radio-group>
|
||||
|
@ -52,8 +52,8 @@ To set the initial checked state, use the `checked` attribute.
|
|||
import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" value="1">
|
||||
<SlRadio name="option" value="1">
|
||||
<SlRadioGroup label="Select an option">
|
||||
<SlRadio name="option" value="1" checked>
|
||||
Option 1
|
||||
</SlRadio>
|
||||
<SlRadio name="option" value="2">
|
||||
|
@ -71,8 +71,8 @@ const App = () => (
|
|||
Use the `disabled` attribute to disable a radio.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" value="1">
|
||||
<sl-radio name="option" value="1">Option 1</sl-radio>
|
||||
<sl-radio-group label="Select an option">
|
||||
<sl-radio name="option" value="1" checked>Option 1</sl-radio>
|
||||
<sl-radio name="option" value="2">Option 2</sl-radio>
|
||||
<sl-radio name="option" value="3" disabled>Option 3</sl-radio>
|
||||
</sl-radio-group>
|
||||
|
@ -82,8 +82,8 @@ Use the `disabled` attribute to disable a radio.
|
|||
import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" value="1">
|
||||
<SlRadio name="option" value="1">
|
||||
<SlRadioGroup label="Select an option">
|
||||
<SlRadio name="option" value="1" checked>
|
||||
Option 1
|
||||
</SlRadio>
|
||||
<SlRadio name="option" value="2">
|
||||
|
@ -96,4 +96,77 @@ const App = () => (
|
|||
);
|
||||
```
|
||||
|
||||
### Custom Validity
|
||||
|
||||
Use the `setCustomValidity()` method to set a custom validation message. This will prevent the form from submitting and make the browser display the error message you provide. To clear the error, call this function with an empty string.
|
||||
|
||||
```html preview
|
||||
<form class="custom-validity">
|
||||
<sl-radio-group label="Select an option">
|
||||
<sl-radio name="a" value="1" checked>Not me</sl-radio>
|
||||
<sl-radio name="a" value="2">Me neither</sl-radio>
|
||||
<sl-radio name="a" value="3">Choose me</sl-radio>
|
||||
</sl-radio-group>
|
||||
<br />
|
||||
<sl-button type="submit" variant="primary">Submit</sl-button>
|
||||
</form>
|
||||
<script>
|
||||
const form = document.querySelector('.custom-validity');
|
||||
const radio = form.querySelectorAll('sl-radio')[2];
|
||||
const errorMessage = 'You must choose this option';
|
||||
// Set initial validity as soon as the element is defined
|
||||
customElements.whenDefined('sl-radio').then(() => {
|
||||
radio.setCustomValidity(errorMessage);
|
||||
});
|
||||
// Update validity when a selection is made
|
||||
form.addEventListener('sl-change', () => {
|
||||
const isValid = radio.checked;
|
||||
radio.setCustomValidity(isValid ? '' : errorMessage);
|
||||
});
|
||||
// Handle form submit
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { SlButton, SlIcon, SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
const App = () => {
|
||||
const radio = useRef(null);
|
||||
const errorMessage = 'You must choose this option';
|
||||
function handleChange(event) {
|
||||
radio.current.setCustomValidity(radio.current.checked ? '' : errorMessage);
|
||||
}
|
||||
function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
}
|
||||
useEffect(() => {
|
||||
radio.current.setCustomValidity(errorMessage);
|
||||
}, []);
|
||||
return (
|
||||
<form class="custom-validity" onSubmit={handleSubmit}>
|
||||
<SlRadioGroup label="Select an option">
|
||||
<SlRadio name="a" value="1" checked onSlChange={handleChange}>
|
||||
Not me
|
||||
</SlRadio>
|
||||
<SlRadio name="a" value="2" onSlChange={handleChange}>
|
||||
Me neither
|
||||
</SlRadio>
|
||||
<SlRadio ref={radio} name="a" value="3" onSlChange={handleChange}>
|
||||
Choose me
|
||||
</SlRadio>
|
||||
</SlRadioGroup>
|
||||
<br />
|
||||
<SlButton type="submit" variant="primary">
|
||||
Submit
|
||||
</SlButton>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
[component-metadata:sl-radio]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import styles from './button-group.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
|
@ -19,8 +19,6 @@ export default class SlButtonGroup extends LitElement {
|
|||
|
||||
@query('slot') defaultSlot: HTMLSlotElement;
|
||||
|
||||
@state() disableRole = false;
|
||||
|
||||
/** A label to use for the button group's `aria-label` attribute. */
|
||||
@property() label = '';
|
||||
|
||||
|
@ -67,14 +65,14 @@ export default class SlButtonGroup extends LitElement {
|
|||
<div
|
||||
part="base"
|
||||
class="button-group"
|
||||
role="${this.disableRole ? 'presentation' : 'group'}"
|
||||
role="group"
|
||||
aria-label=${this.label}
|
||||
@focusout=${this.handleBlur}
|
||||
@focusin=${this.handleFocus}
|
||||
@mouseover=${this.handleMouseOver}
|
||||
@mouseout=${this.handleMouseOut}
|
||||
>
|
||||
<slot @slotchange=${this.handleSlotChange} role="none"></slot>
|
||||
<slot @slotchange=${this.handleSlotChange}></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,9 @@ import { customElement, property, query, state } from 'lit/decorators.js';
|
|||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
import { defaultValue } from '../../internal/default-value';
|
||||
import { emit } from '../../internal/event';
|
||||
import { FormSubmitController } from '../../internal/form';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { watch } from '../../internal/watch';
|
||||
import styles from './radio-button.styles';
|
||||
|
@ -16,6 +18,7 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @slot - The radio's label.
|
||||
*
|
||||
* @event sl-blur - Emitted when the button loses focus.
|
||||
* @event sl-change - Emitted when the button's checked state changes.
|
||||
* @event sl-focus - Emitted when the button gains focus.
|
||||
*
|
||||
* @slot - The button's label.
|
||||
|
@ -35,13 +38,17 @@ export default class SlRadioButton extends LitElement {
|
|||
@query('.button') input: HTMLInputElement;
|
||||
@query('.hidden-input') hiddenInput: HTMLInputElement;
|
||||
|
||||
protected readonly formSubmitController = new FormSubmitController(this, {
|
||||
value: (control: SlRadioButton) => (control.checked ? control.value : undefined),
|
||||
defaultValue: (control: SlRadioButton) => control.defaultChecked,
|
||||
setValue: (control: SlRadioButton, checked: boolean) => (control.checked = checked)
|
||||
});
|
||||
private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
|
||||
|
||||
@state() protected hasFocus = false;
|
||||
@state() protected checked = false;
|
||||
|
||||
/** The radio's name attribute. */
|
||||
@property({ reflect: true }) name: string;
|
||||
@property() name: string;
|
||||
|
||||
/** The radio's value attribute. */
|
||||
@property() value: string;
|
||||
|
@ -49,20 +56,47 @@ export default class SlRadioButton extends LitElement {
|
|||
/** Disables the radio. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
/** The button's size. */
|
||||
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
||||
/** Draws the radio in a checked state. */
|
||||
@property({ type: Boolean, reflect: true }) checked = false;
|
||||
|
||||
/** Draws a pill-style button with rounded edges. */
|
||||
@property({ type: Boolean, reflect: true }) pill = false;
|
||||
/**
|
||||
* This will be true when the control is in an invalid state. Validity in radios is determined by the message provided
|
||||
* by the `setCustomValidity` method.
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) invalid = false;
|
||||
|
||||
/** Gets or sets the default value used to reset this element. The initial value corresponds to the one originally specified in the HTML that created this element. */
|
||||
@defaultValue('checked')
|
||||
defaultChecked = false;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.setAttribute('role', 'presentation');
|
||||
this.setAttribute('role', 'radio');
|
||||
}
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
||||
/** Simulates a click on the radio. */
|
||||
click() {
|
||||
this.input.click();
|
||||
}
|
||||
|
||||
/** Sets focus on the radio. */
|
||||
focus(options?: FocusOptions) {
|
||||
this.input.focus(options);
|
||||
}
|
||||
|
||||
/** Removes focus from the radio. */
|
||||
blur() {
|
||||
this.input.blur();
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
reportValidity() {
|
||||
return this.hiddenInput.reportValidity();
|
||||
}
|
||||
|
||||
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
|
||||
setCustomValidity(message: string) {
|
||||
this.hiddenInput.setCustomValidity(message);
|
||||
}
|
||||
|
||||
handleBlur() {
|
||||
|
@ -70,14 +104,10 @@ export default class SlRadioButton extends LitElement {
|
|||
emit(this, 'sl-blur');
|
||||
}
|
||||
|
||||
handleClick(e: MouseEvent) {
|
||||
if (this.disabled) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
handleClick() {
|
||||
if (!this.disabled) {
|
||||
this.checked = true;
|
||||
}
|
||||
|
||||
this.checked = true;
|
||||
}
|
||||
|
||||
handleFocus() {
|
||||
|
@ -85,13 +115,38 @@ export default class SlRadioButton extends LitElement {
|
|||
emit(this, 'sl-focus');
|
||||
}
|
||||
|
||||
@watch('checked')
|
||||
handleCheckedChange() {
|
||||
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
|
||||
|
||||
if (this.hasUpdated) {
|
||||
emit(this, 'sl-change');
|
||||
}
|
||||
}
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
||||
|
||||
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
||||
if (this.hasUpdated) {
|
||||
this.input.disabled = this.disabled;
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
}
|
||||
|
||||
/** The button's size. */
|
||||
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
||||
|
||||
/** Draws a pill-style button with rounded edges. */
|
||||
@property({ type: Boolean, reflect: true }) pill = false;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div part="base" role="presentation">
|
||||
<div part="base">
|
||||
<input class="hidden-input" type="radio" aria-hidden="true" tabindex="-1" />
|
||||
<button
|
||||
part="button"
|
||||
role="radio"
|
||||
aria-checked="${this.checked}"
|
||||
class=${classMap({
|
||||
button: true,
|
||||
'button--default': true,
|
||||
|
@ -107,11 +162,10 @@ export default class SlRadioButton extends LitElement {
|
|||
'button--has-prefix': this.hasSlotController.test('prefix'),
|
||||
'button--has-suffix': this.hasSlotController.test('suffix')
|
||||
})}
|
||||
aria-disabled=${this.disabled}
|
||||
?disabled=${this.disabled}
|
||||
type="button"
|
||||
name=${ifDefined(this.name)}
|
||||
value=${ifDefined(this.value)}
|
||||
tabindex="${this.checked ? '0' : '-1'}"
|
||||
@blur=${this.handleBlur}
|
||||
@focus=${this.handleFocus}
|
||||
@click=${this.handleClick}
|
||||
|
|
|
@ -49,16 +49,4 @@ export default css`
|
|||
content: var(--sl-input-required-content);
|
||||
margin-inline-start: -2px;
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
import { html, LitElement } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { emit } from 'src/internal/event';
|
||||
import { watch } from 'src/internal/watch';
|
||||
import '../../components/button-group/button-group';
|
||||
import styles from './radio-group.styles';
|
||||
import type SlRadio from '../../components/radio/radio';
|
||||
import type SlRadioButton from '../radio-button/radio-button';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
const RADIO_CHILDREN = ['sl-radio', 'sl-radio-button'];
|
||||
|
@ -30,205 +27,94 @@ export default class SlRadioGroup extends LitElement {
|
|||
static styles: CSSResultGroup = styles;
|
||||
|
||||
@query('slot:not([name])') defaultSlot: HTMLSlotElement;
|
||||
@query('.radio-group__validation-input') input: HTMLInputElement;
|
||||
|
||||
@state() private hasButtonGroup = false;
|
||||
@state() private isInvalid = false;
|
||||
@state() private errorMessage = '';
|
||||
@state() private customErrorMessage = '';
|
||||
@state() hasButtonGroup = false;
|
||||
|
||||
/** The radio group label. Required for proper accessibility. Alternatively, you can use the label slot. */
|
||||
@property() label = '';
|
||||
|
||||
/** The selected value of the control. */
|
||||
@property({ reflect: true }) value = '';
|
||||
|
||||
/** The name assigned to the radio controls. */
|
||||
@property() name = 'option';
|
||||
|
||||
/** Shows the fieldset and legend that surrounds the radio group. */
|
||||
@property({ type: Boolean, attribute: 'fieldset', reflect: true }) fieldset = false;
|
||||
|
||||
/** Ensures a child radio is checked before allowing the containing form to submit. */
|
||||
@property({ type: Boolean, reflect: true }) required = false;
|
||||
|
||||
@watch('value')
|
||||
handleValueChange() {
|
||||
if (this.hasUpdated) {
|
||||
emit(this, 'sl-change');
|
||||
this.updateCheckedRadio();
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.preventInvalidSubmit();
|
||||
this.setAttribute('role', 'radiogroup');
|
||||
}
|
||||
|
||||
setCustomValidity(message = '') {
|
||||
this.customErrorMessage = message;
|
||||
this.errorMessage = message;
|
||||
|
||||
if (!message) {
|
||||
this.isInvalid = false;
|
||||
} else {
|
||||
this.isInvalid = true;
|
||||
this.input.setCustomValidity(message);
|
||||
this.showNativeErrorMessage();
|
||||
}
|
||||
}
|
||||
|
||||
get validity(): ValidityState {
|
||||
const hasMissingData = !((this.value && this.required) || !this.required);
|
||||
const hasCustomError = this.customErrorMessage !== '';
|
||||
|
||||
return {
|
||||
badInput: false,
|
||||
customError: hasCustomError,
|
||||
patternMismatch: false,
|
||||
rangeOverflow: false,
|
||||
rangeUnderflow: false,
|
||||
stepMismatch: false,
|
||||
tooLong: false,
|
||||
tooShort: false,
|
||||
typeMismatch: false,
|
||||
valid: hasMissingData || hasCustomError ? false : true,
|
||||
valueMissing: !hasMissingData
|
||||
};
|
||||
}
|
||||
|
||||
reportValidity() {
|
||||
const validity = this.validity;
|
||||
|
||||
this.errorMessage = this.customErrorMessage || validity.valid ? '' : this.input.validationMessage;
|
||||
this.isInvalid = !validity.valid;
|
||||
|
||||
if (!validity.valid) {
|
||||
this.showNativeErrorMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private preventInvalidSubmit() {
|
||||
this.closest('form')?.addEventListener('submit', e => {
|
||||
this.reportValidity();
|
||||
|
||||
if (this.isInvalid) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private showNativeErrorMessage() {
|
||||
this.input.hidden = false;
|
||||
this.input.reportValidity();
|
||||
setTimeout(() => (this.input.hidden = true), 10000);
|
||||
}
|
||||
|
||||
private getAllRadios() {
|
||||
getAllRadios() {
|
||||
return [...this.querySelectorAll(RADIO_CHILDREN.join(','))].filter(el =>
|
||||
RADIO_CHILDREN.includes(el.tagName.toLowerCase())
|
||||
) as SlRadio[];
|
||||
}
|
||||
|
||||
private handleRadioClick(event: MouseEvent) {
|
||||
const target = event.target as SlRadio | SlRadioButton;
|
||||
handleRadioClick(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
const checkedRadio = target.closest(RADIO_CHILDREN.map(selector => `${selector}:not([disabled])`).join(','));
|
||||
|
||||
if (target.disabled) {
|
||||
return;
|
||||
if (checkedRadio) {
|
||||
const radios = this.getAllRadios();
|
||||
radios.forEach(radio => {
|
||||
radio.checked = radio === checkedRadio;
|
||||
radio.input.tabIndex = radio === checkedRadio ? 0 : -1;
|
||||
});
|
||||
}
|
||||
|
||||
this.value = target.value;
|
||||
const radios = this.getAllRadios();
|
||||
radios.forEach(radio => (radio.checked = radio === target));
|
||||
}
|
||||
|
||||
private handleKeyDown(event: KeyboardEvent) {
|
||||
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(event.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const radios = this.getAllRadios().filter(radio => !radio.disabled);
|
||||
const checkedRadio = radios.find(radio => radio.checked) ?? radios[0];
|
||||
const incr = event.key === ' ' ? 0 : ['ArrowUp', 'ArrowLeft'].includes(event.key) ? -1 : 1;
|
||||
let index = radios.indexOf(checkedRadio) + incr;
|
||||
if (index < 0) {
|
||||
index = radios.length - 1;
|
||||
}
|
||||
if (index > radios.length - 1) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
this.getAllRadios().forEach(radio => {
|
||||
radio.checked = false;
|
||||
|
||||
if (!this.hasButtonGroup) {
|
||||
radio.tabIndex = -1;
|
||||
handleKeyDown(event: KeyboardEvent) {
|
||||
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
|
||||
const radios = this.getAllRadios().filter(radio => !radio.disabled);
|
||||
const checkedRadio = radios.find(radio => radio.checked) ?? radios[0];
|
||||
const incr = ['ArrowUp', 'ArrowLeft'].includes(event.key) ? -1 : 1;
|
||||
let index = radios.indexOf(checkedRadio) + incr;
|
||||
if (index < 0) {
|
||||
index = radios.length - 1;
|
||||
}
|
||||
if (index > radios.length - 1) {
|
||||
index = 0;
|
||||
}
|
||||
});
|
||||
|
||||
this.value = radios[index].value;
|
||||
radios[index].checked = true;
|
||||
this.getAllRadios().forEach(radio => {
|
||||
radio.checked = false;
|
||||
radio.input.tabIndex = -1;
|
||||
});
|
||||
|
||||
if (!this.hasButtonGroup) {
|
||||
radios[index].tabIndex = 0;
|
||||
radios[index].focus();
|
||||
} else {
|
||||
radios[index].shadowRoot!.querySelector('button')!.focus();
|
||||
}
|
||||
radios[index].checked = true;
|
||||
radios[index].input.tabIndex = 0;
|
||||
|
||||
event.preventDefault();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private handleSlotChange() {
|
||||
handleSlotChange() {
|
||||
const radios = this.getAllRadios();
|
||||
|
||||
radios.forEach(radio => {
|
||||
radio.name = this.name;
|
||||
radio.checked = radio.value === this.value;
|
||||
});
|
||||
const checkedRadio = radios.find(radio => radio.checked);
|
||||
|
||||
this.hasButtonGroup = radios.some(radio => radio.tagName.toLowerCase() === 'sl-radio-button');
|
||||
|
||||
if (!radios.some(radio => radio.checked)) {
|
||||
if (this.hasButtonGroup) {
|
||||
const buttonRadio = radios[0].shadowRoot!.querySelector('button')!;
|
||||
buttonRadio.tabIndex = 0;
|
||||
} else {
|
||||
radios[0].tabIndex = 0;
|
||||
}
|
||||
radios.forEach(radio => {
|
||||
radio.setAttribute('role', 'radio');
|
||||
radio.input.tabIndex = -1;
|
||||
});
|
||||
|
||||
if (checkedRadio) {
|
||||
checkedRadio.input.tabIndex = 0;
|
||||
} else if (radios.length > 0) {
|
||||
radios[0].input.tabIndex = 0;
|
||||
}
|
||||
|
||||
if (this.hasButtonGroup) {
|
||||
const buttonGroup = this.shadowRoot?.querySelector('sl-button-group');
|
||||
|
||||
if (buttonGroup) {
|
||||
buttonGroup.disableRole = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateCheckedRadio() {
|
||||
const radios = this.getAllRadios();
|
||||
radios.forEach(radio => (radio.checked = radio.value === this.value));
|
||||
}
|
||||
|
||||
render() {
|
||||
const defaultSlot = html`
|
||||
<slot
|
||||
@click=${this.handleRadioClick}
|
||||
@keydown=${this.handleKeyDown}
|
||||
@slotchange=${this.handleSlotChange}
|
||||
role="presentation"
|
||||
></slot>
|
||||
<slot @click=${this.handleRadioClick} @keydown=${this.handleKeyDown} @slotchange=${this.handleSlotChange}></slot>
|
||||
`;
|
||||
|
||||
return html`
|
||||
<fieldset
|
||||
part="base"
|
||||
role="radiogroup"
|
||||
aria-errormessage="radio-error-message"
|
||||
aria-invalid="${this.isInvalid}"
|
||||
class=${classMap({
|
||||
'radio-group': true,
|
||||
'radio-group--has-fieldset': this.fieldset,
|
||||
|
@ -238,12 +124,6 @@ export default class SlRadioGroup extends LitElement {
|
|||
<legend part="label" class="radio-group__label">
|
||||
<slot name="label">${this.label}</slot>
|
||||
</legend>
|
||||
<div class="visually-hidden">
|
||||
<div id="radio-error-message" aria-live="assertive">${this.errorMessage}</div>
|
||||
<label class="radio-group__validation visually-hidden">
|
||||
<input type="text" class="radio-group__validation-input" ?required=${this.required} tabindex="-1" hidden />
|
||||
</label>
|
||||
</div>
|
||||
${this.hasButtonGroup
|
||||
? html`<sl-button-group part="button-group">${defaultSlot}</sl-button-group>`
|
||||
: defaultSlot}
|
||||
|
|
|
@ -8,10 +8,6 @@ export default css`
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
:host(:focus-visible) {
|
||||
outline: 0px;
|
||||
}
|
||||
|
||||
.radio {
|
||||
display: inline-flex;
|
||||
align-items: top;
|
||||
|
@ -84,7 +80,7 @@ export default css`
|
|||
}
|
||||
|
||||
/* Checked + focus */
|
||||
.radio.radio--focused .radio__control {
|
||||
.radio.radio--checked:not(.radio--disabled) .radio__input:focus-visible ~ .radio__control {
|
||||
outline: var(--sl-focus-ring);
|
||||
outline-offset: var(--sl-focus-ring-offset);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { html, LitElement } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { emit } from 'src/internal/event';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { live } from 'lit/directives/live.js';
|
||||
import { defaultValue } from '../../internal/default-value';
|
||||
import { emit } from '../../internal/event';
|
||||
import { FormSubmitController } from '../../internal/form';
|
||||
import { watch } from '../../internal/watch';
|
||||
import styles from './radio.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
@ -27,12 +31,16 @@ export default class SlRadio extends LitElement {
|
|||
|
||||
@query('.radio__input') input: HTMLInputElement;
|
||||
|
||||
protected readonly formSubmitController = new FormSubmitController(this, {
|
||||
value: (control: SlRadio) => (control.checked ? control.value || 'on' : undefined),
|
||||
defaultValue: (control: SlRadio) => control.defaultChecked,
|
||||
setValue: (control: SlRadio, checked: boolean) => (control.checked = checked)
|
||||
});
|
||||
|
||||
@state() protected hasFocus = false;
|
||||
|
||||
@state() checked = false;
|
||||
|
||||
/** The radio's name attribute. */
|
||||
@property({ reflect: true }) name: string;
|
||||
@property() name: string;
|
||||
|
||||
/** The radio's value attribute. */
|
||||
@property() value: string;
|
||||
|
@ -40,54 +48,118 @@ export default class SlRadio extends LitElement {
|
|||
/** Disables the radio. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
/** Draws the radio in a checked state. */
|
||||
@property({ type: Boolean, reflect: true }) checked = false;
|
||||
|
||||
/**
|
||||
* This will be true when the control is in an invalid state. Validity in radios is determined by the message provided
|
||||
* by the `setCustomValidity` method.
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) invalid = false;
|
||||
|
||||
/** Gets or sets the default value used to reset this element. The initial value corresponds to the one originally specified in the HTML that created this element. */
|
||||
@defaultValue('checked')
|
||||
defaultChecked = false;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.setInitialAttributes();
|
||||
this.addEventListeners();
|
||||
this.setAttribute('role', 'radio');
|
||||
}
|
||||
|
||||
@watch('checked')
|
||||
handleCheckedChange() {
|
||||
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
|
||||
this.setAttribute('tabindex', this.checked ? '0' : '-1');
|
||||
/** Simulates a click on the radio. */
|
||||
click() {
|
||||
this.input.click();
|
||||
}
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
||||
/** Sets focus on the radio. */
|
||||
focus(options?: FocusOptions) {
|
||||
this.input.focus(options);
|
||||
}
|
||||
|
||||
private handleBlur() {
|
||||
/** Removes focus from the radio. */
|
||||
blur() {
|
||||
this.input.blur();
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
reportValidity(): boolean {
|
||||
const group = this.closest('sl-radio-group');
|
||||
const allRadios = group?.getAllRadios().filter(radio => !radio.disabled);
|
||||
const isRequired = group?.required;
|
||||
const isChecked = allRadios?.some(radio => radio.checked);
|
||||
const internalRadio = (radio: SlRadio): HTMLInputElement =>
|
||||
radio.shadowRoot!.querySelector<HTMLInputElement>('input[type="radio"]')!;
|
||||
|
||||
// If no radio group or radios are found, skip validation
|
||||
if (!group || !allRadios) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the radio group is required but no radios are checked, mark the first internal radio required and report it
|
||||
if (isRequired && !isChecked) {
|
||||
const radio = internalRadio(allRadios[0]);
|
||||
radio.required = true;
|
||||
return radio.reportValidity();
|
||||
}
|
||||
|
||||
// Reset the required state of all internal radios so we can accurately report custom validation messages
|
||||
allRadios.forEach(radio => (internalRadio(radio).required = false));
|
||||
|
||||
// Report custom validation errors
|
||||
for (const radio of allRadios) {
|
||||
if (!internalRadio(radio).reportValidity()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
|
||||
setCustomValidity(message: string) {
|
||||
this.input.setCustomValidity(message);
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
handleBlur() {
|
||||
this.hasFocus = false;
|
||||
emit(this, 'sl-blur');
|
||||
}
|
||||
|
||||
private handleClick() {
|
||||
handleClick() {
|
||||
if (!this.disabled) {
|
||||
this.checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
private handleFocus() {
|
||||
handleFocus() {
|
||||
this.hasFocus = true;
|
||||
emit(this, 'sl-focus');
|
||||
}
|
||||
|
||||
private addEventListeners() {
|
||||
this.addEventListener('blur', () => this.handleBlur())
|
||||
this.addEventListener('click', () => this.handleClick())
|
||||
this.addEventListener('focus', () => this.handleFocus())
|
||||
@watch('checked')
|
||||
handleCheckedChange() {
|
||||
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
|
||||
|
||||
if (this.hasUpdated) {
|
||||
emit(this, 'sl-change');
|
||||
}
|
||||
}
|
||||
|
||||
private setInitialAttributes() {
|
||||
this.setAttribute('role', 'radio');
|
||||
this.setAttribute('tabindex', '-1');
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
||||
|
||||
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
||||
if (this.hasUpdated) {
|
||||
this.input.disabled = this.disabled;
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<span
|
||||
<label
|
||||
part="base"
|
||||
class=${classMap({
|
||||
radio: true,
|
||||
|
@ -96,6 +168,17 @@ export default class SlRadio extends LitElement {
|
|||
'radio--focused': this.hasFocus
|
||||
})}
|
||||
>
|
||||
<input
|
||||
class="radio__input"
|
||||
type="radio"
|
||||
name=${ifDefined(this.name)}
|
||||
value=${ifDefined(this.value)}
|
||||
.checked=${live(this.checked)}
|
||||
.disabled=${this.disabled}
|
||||
@click=${this.handleClick}
|
||||
@blur=${this.handleBlur}
|
||||
@focus=${this.handleFocus}
|
||||
/>
|
||||
<span part="control" class="radio__control">
|
||||
<svg part="checked-icon" class="radio__icon" viewBox="0 0 16 16">
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
|
|
Ładowanie…
Reference in New Issue