diff --git a/docs/_sidebar.md b/docs/_sidebar.md
index 4cf6a8351..9e10d813e 100644
--- a/docs/_sidebar.md
+++ b/docs/_sidebar.md
@@ -49,6 +49,7 @@
- [Progress Ring](/components/progress-ring)
- [QR Code](/components/qr-code)
- [Radio](/components/radio)
+ - [Radio Button](/components/radio-button)
- [Radio Group](/components/radio-group)
- [Range](/components/range)
- [Rating](/components/rating)
diff --git a/docs/components/radio-button.md b/docs/components/radio-button.md
new file mode 100644
index 000000000..4c8f43847
--- /dev/null
+++ b/docs/components/radio-button.md
@@ -0,0 +1,381 @@
+# Radio Button
+
+[component-header:sl-radio-button]
+
+Radios buttons allow the user to select a single option from a group using a button-like control.
+
+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
+
+ Option 1
+ Option 2
+ Option 3
+
+```
+
+```jsx react
+import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
+
+const App = () => (
+
+
+ Option 1
+
+
+ Option 2
+
+
+ Option 3
+
+
+);
+```
+
+## Examples
+
+### Checked
+
+To set the initial checked state, use the `checked` attribute.
+
+```html preview
+
+ Option 1
+ Option 2
+ Option 3
+
+```
+
+```jsx react
+import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
+
+const App = () => (
+
+
+ Option 1
+
+
+ Option 2
+
+
+ Option 3
+
+
+);
+```
+
+### Disabled
+
+Use the `disabled` attribute to disable a radio button.
+
+```html preview
+
+ Option 1
+ Option 2
+ Option 3
+
+```
+
+```jsx react
+import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
+
+const App = () => (
+
+
+ Option 1
+
+
+ Option 2
+
+
+ Option 3
+
+
+);
+```
+
+### Variants
+
+Use the `variant` attribute to set the button's variant.
+
+```html preview
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+
+ Option 1
+ Option 2
+ Option 3
+
+```
+
+```jsx react
+import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
+
+const App = () => (
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+
+ Option 1
+ Option 2
+ Option 3
+
+);
+```
+
+### Sizes
+
+Use the `size` attribute to change a radio button's size.
+
+```html preview
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+
+ Option 1
+ Option 2
+ Option 3
+
+```
+
+```jsx react
+import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
+
+const App = () => (
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+
+ Option 1
+ Option 2
+ Option 3
+
+);
+```
+
+### Pill Buttons
+
+Use the `pill` attribute to give radio buttons rounded edges.
+
+```html preview
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+
+ Option 1
+ Option 2
+ Option 3
+
+```
+
+```jsx react
+import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
+
+const App = () => (
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+
+ Option 1
+ Option 2
+ Option 3
+
+);
+```
+
+### Prefix and Suffix Icons
+
+Use the `prefix` and `suffix` slots to add icons.
+
+```html preview
+
+
+
+ Option 1
+
+
+
+
+ Option 2
+
+
+
+
+
+ Option 3
+
+
+```
+
+```jsx react
+import { SlIcon, SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
+
+const App = () => (
+
+
+
+ Option 1
+
+
+
+
+ Option 2
+
+
+
+
+
+ Option 3
+
+
+);
+```
+
+### Buttons with Icons
+
+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
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+[component-metadata:sl-radio-button]
diff --git a/docs/components/radio-group.md b/docs/components/radio-group.md
index c67cb9db6..d5a28963d 100644
--- a/docs/components/radio-group.md
+++ b/docs/components/radio-group.md
@@ -2,7 +2,7 @@
[component-header:sl-radio-group]
-Radio Groups are used to group multiple radios so they function as a single control.
+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
@@ -32,9 +32,9 @@ const App = () => (
## Examples
-### Showing the Fieldset
+### Showing the Label
-You can show a fieldset and legend that wraps the radio group using the `fieldset` attribute.
+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
@@ -62,4 +62,34 @@ const App = () => (
);
```
+### Radio Buttons
+
+[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
+
+ Option 1
+ Option 2
+ Option 3
+
+```
+
+```jsx react
+import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
+
+const App = () => (
+
+
+ Option 1
+
+
+ Option 2
+
+
+ Option 3
+
+
+);
+```
+
[component-metadata:sl-radio-group]
diff --git a/docs/components/radio.md b/docs/components/radio.md
index 025b467b7..4c271e972 100644
--- a/docs/components/radio.md
+++ b/docs/components/radio.md
@@ -2,9 +2,9 @@
[component-header:sl-radio]
-Radios allow the user to select one option from a group of many.
+Radios allow the user to select a single option from a group.
-Radios are designed to be used with [radio groups](/components/radio-group). As such, all of the examples on this page utilize them to demonstrate their correct usage.
+Radios are designed to be used with [radio groups](/components/radio-group).
```html preview
@@ -36,16 +36,15 @@ const App = () => (
## Examples
-### Disabled
+### Checked
-Use the `disabled` attribute to disable a radio.
+To set the initial checked state, use the `checked` attribute.
```html preview
Option 1
Option 2
Option 3
- Disabled
```
@@ -63,8 +62,35 @@ const App = () => (
Option 3
-
- Disabled
+
+);
+```
+
+### Disabled
+
+Use the `disabled` attribute to disable a radio.
+
+```html preview
+
+ Option 1
+ Option 2
+ Option 3
+
+```
+
+```jsx react
+import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
+
+const App = () => (
+
+
+ Option 1
+
+
+ Option 2
+
+
+ Option 3
);
diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md
index 88e1a722a..ee4558bfa 100644
--- a/docs/resources/changelog.md
+++ b/docs/resources/changelog.md
@@ -8,6 +8,8 @@ _During the beta period, these restrictions may be relaxed in the event of a mis
## Next
+- Added the experimental `` component
+- Added `button-group` and `button-group__base` parts to ``
- Fixed a bug that prevented form submission from working as expected in some cases
- Fixed a bug that prevented `` from toggling `vertical` properly [#703](https://github.com/shoelace-style/shoelace/issues/703)
- Fixed a bug that prevented `` from rendering a color initially [#704](https://github.com/shoelace-style/shoelace/issues/704)
@@ -125,7 +127,7 @@ _During the beta period, these restrictions may be relaxed in the event of a mis
- 🚨 BREAKING: changed the `type` attribute to `variant` in ``, ``, ``, and `` since it's more appropriate and to disambiguate from other `type` attributes
- 🚨 BREAKING: removed `base` part from `` to simplify the styling API
-- Added experimental `` component
+- Added the experimental `` component
- Added `focus()` and `blur()` methods to `` [#625](https://github.com/shoelace-style/shoelace/pull/625)
- Fixed a bug where setting `tooltipFormatter` on `` in JSX causes React@experimental to error out
- Fixed a bug where clicking on a slotted icon in `` wouldn't submit forms [#626](https://github.com/shoelace-style/shoelace/issues/626)
diff --git a/src/components/button-group/button-group.ts b/src/components/button-group/button-group.ts
index 610557657..cba1347c2 100644
--- a/src/components/button-group/button-group.ts
+++ b/src/components/button-group/button-group.ts
@@ -2,6 +2,8 @@ import { LitElement, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js';
import styles from './button-group.styles';
+const BUTTON_CHILDREN = ['sl-button', 'sl-radio-button'];
+
/**
* @since 2.0
* @status stable
@@ -75,7 +77,7 @@ export default class SlButtonGroup extends LitElement {
}
function findButton(el: HTMLElement) {
- return el.tagName.toLowerCase() === 'sl-button' ? el : el.querySelector('sl-button');
+ return BUTTON_CHILDREN.includes(el.tagName.toLowerCase()) ? el : el.querySelector(BUTTON_CHILDREN.join(','));
}
declare global {
diff --git a/src/components/button/button.styles.ts b/src/components/button/button.styles.ts
index 283de7842..aa7c46875 100644
--- a/src/components/button/button.styles.ts
+++ b/src/components/button/button.styles.ts
@@ -26,8 +26,8 @@ export default css`
white-space: nowrap;
vertical-align: middle;
padding: 0;
- transition: var(--sl-transition-fast) background-color, var(--sl-transition-fast) color,
- var(--sl-transition-fast) border, var(--sl-transition-fast) box-shadow;
+ transition: var(--sl-transition-x-fast) background-color, var(--sl-transition-x-fast) color,
+ var(--sl-transition-x-fast) border, var(--sl-transition-x-fast) box-shadow;
cursor: inherit;
}
@@ -235,7 +235,8 @@ export default css`
color: var(--sl-color-neutral-700);
}
- .button--outline.button--default:hover:not(.button--disabled) {
+ .button--outline.button--default:hover:not(.button--disabled),
+ .button--outline.button--default.button--checked:not(.button--disabled) {
border-color: var(--sl-color-primary-600);
background-color: var(--sl-color-primary-600);
color: var(--sl-color-neutral-0);
@@ -258,7 +259,8 @@ export default css`
color: var(--sl-color-primary-600);
}
- .button--outline.button--primary:hover:not(.button--disabled) {
+ .button--outline.button--primary:hover:not(.button--disabled),
+ .button--outline.button--primary.button--checked:not(.button--disabled) {
background-color: var(--sl-color-primary-600);
color: var(--sl-color-neutral-0);
}
@@ -280,7 +282,8 @@ export default css`
color: var(--sl-color-success-600);
}
- .button--outline.button--success:hover:not(.button--disabled) {
+ .button--outline.button--success:hover:not(.button--disabled),
+ .button--outline.button--success.button--checked:not(.button--disabled) {
background-color: var(--sl-color-success-600);
color: var(--sl-color-neutral-0);
}
@@ -302,7 +305,8 @@ export default css`
color: var(--sl-color-neutral-600);
}
- .button--outline.button--neutral:hover:not(.button--disabled) {
+ .button--outline.button--neutral:hover:not(.button--disabled),
+ .button--outline.button--neutral.button--checked:not(.button--disabled) {
background-color: var(--sl-color-neutral-600);
color: var(--sl-color-neutral-0);
}
@@ -324,7 +328,8 @@ export default css`
color: var(--sl-color-warning-600);
}
- .button--outline.button--warning:hover:not(.button--disabled) {
+ .button--outline.button--warning:hover:not(.button--disabled),
+ .button--outline.button--warning.button--checked:not(.button--disabled) {
background-color: var(--sl-color-warning-600);
color: var(--sl-color-neutral-0);
}
@@ -346,7 +351,8 @@ export default css`
color: var(--sl-color-danger-600);
}
- .button--outline.button--danger:hover:not(.button--disabled) {
+ .button--outline.button--danger:hover:not(.button--disabled),
+ .button--outline.button--danger.button--checked:not(.button--disabled) {
background-color: var(--sl-color-danger-600);
color: var(--sl-color-neutral-0);
}
diff --git a/src/components/button/button.test.ts b/src/components/button/button.test.ts
index 100d74f95..1846f5420 100644
--- a/src/components/button/button.test.ts
+++ b/src/components/button/button.test.ts
@@ -126,8 +126,6 @@ describe('', () => {
const button = el.querySelector('sl-button')!;
const handleSubmit = sinon.spy((event: SubmitEvent) => event.preventDefault());
- console.log(form, button);
-
form.addEventListener('submit', handleSubmit);
button.click();
diff --git a/src/components/button/button.ts b/src/components/button/button.ts
index 52e3f73ea..d98ce3dcd 100644
--- a/src/components/button/button.ts
+++ b/src/components/button/button.ts
@@ -23,9 +23,9 @@ import styles from './button.styles';
* @slot suffix - Used to append an icon or similar element to the button.
*
* @csspart base - The component's internal wrapper.
- * @csspart prefix - The prefix container.
+ * @csspart prefix - The prefix slot's container.
* @csspart label - The button's label.
- * @csspart suffix - The suffix container.
+ * @csspart suffix - The suffix slot's container.
* @csspart caret - The button's caret.
*/
@customElement('sl-button')
diff --git a/src/components/radio-button/radio-button.styles.ts b/src/components/radio-button/radio-button.styles.ts
new file mode 100644
index 000000000..fd001391b
--- /dev/null
+++ b/src/components/radio-button/radio-button.styles.ts
@@ -0,0 +1,10 @@
+import { css } from 'lit';
+import componentStyles from '~/styles/component.styles';
+
+export default css`
+ ${componentStyles}
+
+ :host {
+ display: block;
+ }
+`;
diff --git a/src/components/radio-button/radio-button.test.ts b/src/components/radio-button/radio-button.test.ts
new file mode 100644
index 000000000..5c580aeb3
--- /dev/null
+++ b/src/components/radio-button/radio-button.test.ts
@@ -0,0 +1,99 @@
+import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
+import { sendKeys } from '@web/test-runner-commands';
+import sinon from 'sinon';
+import type SlRadioGroup from '~/components/radio-group/radio-group';
+import type SlRadioButton from './radio-button';
+
+describe('', () => {
+ it('should be disabled with the disabled attribute', async () => {
+ const el = await fixture(html` `);
+
+ expect(el.input.disabled).to.be.true;
+ });
+
+ it('should be valid by default', async () => {
+ const el = await fixture(html` `);
+
+ expect(el.invalid).to.be.false;
+ });
+
+ it('should fire sl-change when clicked', async () => {
+ const el = await fixture(html` `);
+ setTimeout(() => el.input.click());
+ const event = await oneEvent(el, 'sl-change');
+ expect(event.target).to.equal(el);
+ expect(el.checked).to.be.true;
+ });
+
+ it('should fire sl-change when toggled via keyboard - space', async () => {
+ const el = await fixture(html` `);
+ el.input.focus();
+ setTimeout(() => sendKeys({ press: ' ' }));
+ const event = await oneEvent(el, 'sl-change');
+ expect(event.target).to.equal(el);
+ expect(el.checked).to.be.true;
+ });
+
+ it('should fire sl-change when toggled via keyboard - arrow key', async () => {
+ const radioGroup = await fixture(html`
+
+
+
+
+ `);
+ const radio1 = radioGroup.querySelector('#radio-1')!;
+ const radio2 = radioGroup.querySelector('#radio-2')!;
+ radio1.input.focus();
+ setTimeout(() => sendKeys({ press: 'ArrowRight' }));
+ const event = await oneEvent(radio2, 'sl-change');
+ expect(event.target).to.equal(radio2);
+ expect(radio2.checked).to.be.true;
+ });
+
+ it('should not get checked when disabled', async () => {
+ const radioGroup = await fixture(html`
+
+
+
+
+ `);
+ const radio1 = radioGroup.querySelector('sl-radio-button[checked]')!;
+ const radio2 = radioGroup.querySelector('sl-radio-button[disabled]')!;
+
+ radio2.click();
+ await Promise.all([radio1.updateComplete, radio2.updateComplete]);
+
+ expect(radio1.checked).to.be.true;
+ expect(radio2.checked).to.be.false;
+ });
+
+ describe('when submitting a form', () => {
+ it('should submit the correct value', async () => {
+ const form = await fixture(html`
+
+ `);
+ const button = form.querySelector('sl-button')!;
+ const radio = form.querySelectorAll('sl-radio-button')[1]!;
+ const submitHandler = sinon.spy((event: SubmitEvent) => {
+ formData = new FormData(form);
+ event.preventDefault();
+ });
+ let formData: FormData;
+
+ form.addEventListener('submit', submitHandler);
+ radio.click();
+ button.click();
+
+ await waitUntil(() => submitHandler.calledOnce);
+
+ expect(formData!.get('a')).to.equal('2');
+ });
+ });
+});
diff --git a/src/components/radio-button/radio-button.ts b/src/components/radio-button/radio-button.ts
new file mode 100644
index 000000000..9dcf33456
--- /dev/null
+++ b/src/components/radio-button/radio-button.ts
@@ -0,0 +1,100 @@
+import { customElement, property } 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 styles from '~/components/button/button.styles';
+import RadioBase from '~/internal/radio';
+import { HasSlotController } from '~/internal/slot';
+
+/**
+ * @since 2.0
+ * @status stable
+ *
+ * @event sl-blur - Emitted when the button loses focus.
+ * @event sl-focus - Emitted when the button gains focus.
+ *
+ * @slot - The button's label.
+ * @slot prefix - Used to prepend an icon or similar element to the button.
+ * @slot suffix - Used to append an icon or similar element to the button.
+ *
+ * @csspart base - The component's internal wrapper.
+ * @csspart prefix - The prefix slot's container.
+ * @csspart label - The button's label.
+ * @csspart suffix - The suffix slot's container.
+ */
+@customElement('sl-radio-button')
+export default class SlRadioButton extends RadioBase {
+ static styles = styles;
+
+ private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
+
+ /** The button's variant. */
+ @property({ reflect: true }) variant: 'default' | 'primary' | 'success' | 'neutral' | 'warning' | 'danger' =
+ 'default';
+
+ /** The button's size. */
+ @property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
+
+ /**
+ * 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.
+ */
+ @property({ type: Boolean, reflect: true }) invalid = false;
+
+ /** Draws a pill-style button with rounded edges. */
+ @property({ type: Boolean, reflect: true }) pill = false;
+
+ render() {
+ return html`
+
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'sl-radio-button': SlRadioButton;
+ }
+}
diff --git a/src/components/radio-group/radio-group.ts b/src/components/radio-group/radio-group.ts
index 021be275a..7431f24dc 100644
--- a/src/components/radio-group/radio-group.ts
+++ b/src/components/radio-group/radio-group.ts
@@ -1,19 +1,25 @@
import { html, LitElement } from 'lit';
-import { customElement, property, query } from 'lit/decorators.js';
+import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
+import '~/components/button-group/button-group';
import type SlRadio from '~/components/radio/radio';
-import { emit } from '~/internal/event';
import styles from './radio-group.styles';
+const RADIO_CHILDREN = ['sl-radio', 'sl-radio-button'];
+
/**
* @since 2.0
* @status stable
*
+ * @dependency sl-button-group
+ *
* @slot - The default slot where radio controls are placed.
* @slot label - The radio group label. Required for proper accessibility. Alternatively, you can use the label prop.
*
* @csspart base - The component's internal wrapper.
- * @csspart label - The radio group label.
+ * @csspart label - The radio group's label.
+ * @csspart button-group - The button group that wraps radio buttons.
+ * @csspart button-group__base - The button group's `base` part.
*/
@customElement('sl-radio-group')
export default class SlRadioGroup extends LitElement {
@@ -21,6 +27,8 @@ export default class SlRadioGroup extends LitElement {
@query('slot:not([name])') defaultSlot: HTMLSlotElement;
+ @state() hasButtonGroup = false;
+
/** The radio group label. Required for proper accessibility. Alternatively, you can use the label slot. */
@property() label = '';
@@ -33,14 +41,14 @@ export default class SlRadioGroup extends LitElement {
}
getAllRadios() {
- return this.defaultSlot
- .assignedElements({ flatten: true })
- .filter(el => el.tagName.toLowerCase() === 'sl-radio') as SlRadio[];
+ return [...this.querySelectorAll(RADIO_CHILDREN.join(','))].filter(el =>
+ RADIO_CHILDREN.includes(el.tagName.toLowerCase())
+ ) as SlRadio[];
}
handleRadioClick(event: MouseEvent) {
const target = event.target as HTMLElement;
- const checkedRadio = target.closest('sl-radio');
+ const checkedRadio = target.closest(RADIO_CHILDREN.map(selector => `${selector}:not([disabled])`).join(','));
if (checkedRadio) {
const radios = this.getAllRadios();
@@ -73,8 +81,6 @@ export default class SlRadioGroup extends LitElement {
radios[index].checked = true;
radios[index].input.tabIndex = 0;
- emit(radios[index], 'sl-change');
-
event.preventDefault();
}
}
@@ -83,6 +89,8 @@ export default class SlRadioGroup extends LitElement {
const radios = this.getAllRadios();
const checkedRadio = radios.find(radio => radio.checked);
+ this.hasButtonGroup = !!radios.find(radio => radio.tagName.toLowerCase() === 'sl-radio-button');
+
radios.forEach(radio => {
radio.setAttribute('role', 'radio');
radio.input.tabIndex = -1;
@@ -96,6 +104,10 @@ export default class SlRadioGroup extends LitElement {
}
render() {
+ const defaultSlot = html`
+
+ `;
+
return html`
`;
}
diff --git a/src/components/radio/radio.test.ts b/src/components/radio/radio.test.ts
index e05f4a364..12c86e634 100644
--- a/src/components/radio/radio.test.ts
+++ b/src/components/radio/radio.test.ts
@@ -7,7 +7,7 @@ import type SlRadio from './radio';
describe('', () => {
it('should be disabled with the disabled attribute', async () => {
const el = await fixture(html` `);
- const radio = el.shadowRoot!.querySelector('input')!;
+ const radio = el.input;
expect(radio.disabled).to.be.true;
});
@@ -20,7 +20,7 @@ describe('', () => {
it('should fire sl-change when clicked', async () => {
const el = await fixture(html` `);
- setTimeout(() => el.shadowRoot!.querySelector('input')!.click());
+ setTimeout(() => el.input.click());
const event = await oneEvent(el, 'sl-change');
expect(event.target).to.equal(el);
expect(el.checked).to.be.true;
@@ -28,8 +28,7 @@ describe('', () => {
it('should fire sl-change when toggled via keyboard - space', async () => {
const el = await fixture(html` `);
- const input = el.shadowRoot!.querySelector('input')!;
- input.focus();
+ el.input.focus();
setTimeout(() => sendKeys({ press: ' ' }));
const event = await oneEvent(el, 'sl-change');
expect(event.target).to.equal(el);
@@ -43,16 +42,32 @@ describe('', () => {
`);
- const radio1 = radioGroup.querySelector('sl-radio#radio-1')!;
- const radio2 = radioGroup.querySelector('sl-radio#radio-2')!;
- const input1 = radio1.shadowRoot!.querySelector('input')!;
- input1.focus();
+ const radio1 = radioGroup.querySelector('#radio-1')!;
+ const radio2 = radioGroup.querySelector('#radio-2')!;
+ radio1.input.focus();
setTimeout(() => sendKeys({ press: 'ArrowRight' }));
const event = await oneEvent(radio2, 'sl-change');
expect(event.target).to.equal(radio2);
expect(radio2.checked).to.be.true;
});
+ it('should not get checked when disabled', async () => {
+ const radioGroup = await fixture(html`
+
+
+
+
+ `);
+ const radio1 = radioGroup.querySelector('sl-radio[checked]')!;
+ const radio2 = radioGroup.querySelector('sl-radio[disabled]')!;
+
+ radio2.click();
+ await Promise.all([radio1.updateComplete, radio2.updateComplete]);
+
+ expect(radio1.checked).to.be.true;
+ expect(radio2.checked).to.be.false;
+ });
+
describe('when submitting a form', () => {
it('should submit the correct value', async () => {
const form = await fixture(html`
diff --git a/src/components/radio/radio.ts b/src/components/radio/radio.ts
index ac0d952bb..6387da42e 100644
--- a/src/components/radio/radio.ts
+++ b/src/components/radio/radio.ts
@@ -1,11 +1,9 @@
-import { html, LitElement } from 'lit';
-import { customElement, property, query, state } from 'lit/decorators.js';
+import { html } from 'lit';
+import { customElement } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js';
-import { emit } from '~/internal/event';
-import { FormSubmitController } from '~/internal/form-control';
-import { watch } from '~/internal/watch';
+import RadioBase from '~/internal/radio';
import styles from './radio.styles';
/**
@@ -24,112 +22,9 @@ import styles from './radio.styles';
* @csspart label - The radio label.
*/
@customElement('sl-radio')
-export default class SlRadio extends LitElement {
+export default class SlRadio extends RadioBase {
static styles = styles;
- @query('input[type="radio"]') input: HTMLInputElement;
-
- // @ts-expect-error -- Controller is currently unused
- private readonly formSubmitController = new FormSubmitController(this, {
- value: (control: SlRadio) => (control.checked ? control.value : undefined)
- });
-
- @state() private hasFocus = false;
-
- /** The radio's name attribute. */
- @property() name: string;
-
- /** The radio's value attribute. */
- @property() value: string;
-
- /** 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 range inputs is determined by the message
- * provided by the `setCustomValidity` method.
- */
- @property({ type: Boolean, reflect: true }) invalid = false;
-
- connectedCallback(): void {
- super.connectedCallback();
- this.setAttribute('role', 'radio');
- }
-
- /** 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.input.reportValidity();
- }
-
- /** 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();
- }
-
- getAllRadios() {
- const radioGroup = this.closest('sl-radio-group');
-
- // Radios must be part of a radio group
- if (radioGroup === null) {
- return [this];
- }
-
- return [...radioGroup.querySelectorAll('sl-radio')].filter((radio: this) => radio.name === this.name);
- }
-
- handleBlur() {
- this.hasFocus = false;
- emit(this, 'sl-blur');
- }
-
- @watch('checked')
- handleCheckedChange() {
- this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
-
- if (this.hasUpdated) {
- emit(this, 'sl-change');
- }
- }
-
- handleClick() {
- this.checked = true;
- }
-
- @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();
- }
- }
-
- handleFocus() {
- this.hasFocus = true;
- emit(this, 'sl-focus');
- }
-
render() {
return html`