` to allow you to customize the label and help text position
diff --git a/src/components/button/button.styles.ts b/src/components/button/button.styles.ts
index 289fa63b..181fa2b8 100644
--- a/src/components/button/button.styles.ts
+++ b/src/components/button/button.styles.ts
@@ -632,12 +632,13 @@ export default css`
mix-blend-mode: multiply;
}
- /* Bump focused buttons up so their focus ring isn't clipped */
+ /* Bump hovered, focused, and checked buttons up so their focus ring isn't clipped */
:host(.sl-button-group__button--hover) {
z-index: 1;
}
- :host(.sl-button-group__button--focus) {
+ :host(.sl-button-group__button--focus),
+ :host(.sl-button-group__button[checked]) {
z-index: 2;
}
`;
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 00000000..21e55233
--- /dev/null
+++ b/src/components/radio-button/radio-button.styles.ts
@@ -0,0 +1,25 @@
+import { css } from 'lit';
+import buttonStyles from '../button/button.styles';
+
+export default css`
+ ${buttonStyles}
+
+ label {
+ display: inline-block;
+ position: relative;
+ }
+
+ /* We use a hidden input so constraint validation errors work, since they don't appear to show when used with buttons.
+ We can't actually hide it, though, otherwise the messages will be suppressed by the browser. */
+ .hidden-input {
+ all: unset;
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ outline: dotted 1px red;
+ opacity: 0;
+ z-index: -1;
+ }
+`;
diff --git a/src/components/radio-button/radio-button.ts b/src/components/radio-button/radio-button.ts
index fbccddba..3acd6bf5 100644
--- a/src/components/radio-button/radio-button.ts
+++ b/src/components/radio-button/radio-button.ts
@@ -1,10 +1,13 @@
-import { customElement, property } from 'lit/decorators.js';
+import { LitElement } from 'lit';
+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 styles from '../../components/button/button.styles';
-import RadioBase from '../../internal/radio';
+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';
/**
* @since 2.0
@@ -21,16 +24,109 @@ import { HasSlotController } from '../../internal/slot';
* @slot suffix - Used to append an icon or similar element to the button.
*
* @csspart base - The component's internal wrapper.
+ * @csspart button - The internal button element.
* @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 {
+export default class SlRadioButton extends LitElement {
static styles = styles;
+ @query('.button') input: HTMLInputElement;
+ @query('.hidden-input') hiddenInput: HTMLInputElement;
+
+ protected readonly formSubmitController = new FormSubmitController(this, {
+ value: (control: SlRadioButton) => (control.checked ? control.value : undefined)
+ });
private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
+ @state() protected 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 radios 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.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() {
+ this.hasFocus = false;
+ emit(this, 'sl-blur');
+ }
+
+ handleClick() {
+ if (!this.disabled) {
+ this.checked = true;
+ }
+ }
+
+ handleFocus() {
+ this.hasFocus = true;
+ 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 variant. */
@property({ reflect: true }) variant: 'default' | 'primary' | 'success' | 'neutral' | 'warning' | 'danger' =
'default';
@@ -38,57 +134,54 @@ export default class SlRadioButton extends RadioBase {
/** 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 radio buttons 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`
-
+
+
+
+
`;
}
}
diff --git a/src/components/radio/radio.ts b/src/components/radio/radio.ts
index c5289f84..26eecd38 100644
--- a/src/components/radio/radio.ts
+++ b/src/components/radio/radio.ts
@@ -1,9 +1,11 @@
-import { html } from 'lit';
-import { customElement } from 'lit/decorators.js';
+import { html, LitElement } from 'lit';
+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 { live } from 'lit/directives/live.js';
-import RadioBase from '../../internal/radio';
+import { emit } from '../../internal/event';
+import { FormSubmitController } from '../../internal/form';
+import { watch } from '../../internal/watch';
import styles from './radio.styles';
/**
@@ -22,9 +24,102 @@ import styles from './radio.styles';
* @csspart label - The radio label.
*/
@customElement('sl-radio')
-export default class SlRadio extends RadioBase {
+export default class SlRadio extends LitElement {
static styles = styles;
+ @query('.radio__input') input: HTMLInputElement;
+
+ protected readonly formSubmitController = new FormSubmitController(this, {
+ value: (control: HTMLInputElement) => (control.checked ? control.value : undefined)
+ });
+
+ @state() protected 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 radios 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();
+ }
+
+ handleBlur() {
+ this.hasFocus = false;
+ emit(this, 'sl-blur');
+ }
+
+ handleClick() {
+ if (!this.disabled) {
+ this.checked = true;
+ }
+ }
+
+ handleFocus() {
+ this.hasFocus = true;
+ 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();
+ }
+ }
+
render() {
return html`