diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 0ed46047..e918c3ee 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -201,7 +201,17 @@ module.exports = {
'no-restricted-imports': [
'warn',
{
+ patterns: [
+ {
+ group: ['../*'],
+ message: 'Usage of relative parent imports is not allowed.'
+ }
+ ],
paths: [
+ {
+ name: '.',
+ message: 'Usage of local index imports is not allowed.'
+ },
{
name: './index',
message: 'Import from the source file instead.'
@@ -214,6 +224,16 @@ module.exports = {
'warn',
{
groups: ['builtin', 'external', 'internal', 'unknown', 'parent', 'sibling', 'index', 'object', 'type'],
+ pathGroups: [
+ {
+ pattern: '~/**',
+ group: 'internal'
+ },
+ {
+ pattern: 'dist/**',
+ group: 'external'
+ }
+ ],
alphabetize: {
order: 'asc',
caseInsensitive: true
diff --git a/cspell.json b/cspell.json
index 1e52dd4a..0f6de2b3 100644
--- a/cspell.json
+++ b/cspell.json
@@ -108,6 +108,7 @@
"textareas",
"transitionend",
"Triaging",
+ "ttsc",
"turbolinks",
"unbundles",
"unbundling",
diff --git a/docs/components/checkbox.md b/docs/components/checkbox.md
index 1a40415e..dbb67e37 100644
--- a/docs/components/checkbox.md
+++ b/docs/components/checkbox.md
@@ -60,73 +60,4 @@ import { SlCheckbox } from '@shoelace-style/shoelace/dist/react';
const App = () => Disabled;
```
-### 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
-
-
-
-```
-
-```jsx react
-import { useEffect, useRef } from 'react';
-import { SlButton, SlCheckbox } from '@shoelace-style/shoelace/dist/react';
-
-const App = () => {
- const checkbox = useRef(null);
- const errorMessage = `Don't forget to check me!`;
-
- function handleChange() {
- checkbox.current.setCustomValidity(checkbox.current.checked ? '' : errorMessage);
- }
-
- function handleSubmit(event) {
- event.preventDefault();
- alert('All fields are valid!');
- }
-
- useEffect(() => {
- checkbox.current.setCustomValidity(errorMessage);
- }, []);
-
- return (
-
- );
-};
-```
-
[component-metadata:sl-checkbox]
diff --git a/docs/components/radio-button.md b/docs/components/radio-button.md
index 7f745b18..a21d138f 100644
--- a/docs/components/radio-button.md
+++ b/docs/components/radio-button.md
@@ -414,90 +414,4 @@ 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
-
-
-
-```
-
-```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 (
-
- );
-};
-```
-
[component-metadata:sl-radio-button]
diff --git a/docs/components/radio.md b/docs/components/radio.md
index c6368374..4c271e97 100644
--- a/docs/components/radio.md
+++ b/docs/components/radio.md
@@ -96,90 +96,4 @@ 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
-
-
-
-```
-
-```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 (
-
- );
-};
-```
-
[component-metadata:sl-radio]
diff --git a/docs/getting-started/form-controls.md b/docs/getting-started/form-controls.md
index b4c1584a..472be696 100644
--- a/docs/getting-started/form-controls.md
+++ b/docs/getting-started/form-controls.md
@@ -194,7 +194,7 @@ const App = () => {
### Custom Validation
-To create a custom validation error, pass a non-empty string to the `setCustomValidity()` method. This will override any existing validation constraints. The form will not be submitted when a custom validity is set and the browser will show a validation error when the containing form is submitted. To make the input valid again, call `setCustomValidity()` again with an empty string.
+To create a custom validation error, use the `setCustomValidity` method. The form will not be submitted when this method is called with anything other than an empty string, and its message will be shown by the browser as the validation error. To make the input valid again, call the method a second time with an empty string as the argument.
```html preview
@@ -95,29 +95,5 @@ describe('', () => {
expect(formData!.get('a')).to.equal('2');
});
-
- it('should show a constraint validation error when setCustomValidity() is called', async () => {
- const form = await fixture(html`
-
-
-
-
-
- Submit
-
- `);
- const button = form.querySelector('sl-button')!;
- const radio = form.querySelectorAll('sl-radio-button')[1]!;
- const submitHandler = sinon.spy((event: SubmitEvent) => event.preventDefault());
-
- // Submitting the form after setting custom validity should not trigger the handler
- radio.setCustomValidity('Invalid selection');
- form.addEventListener('submit', submitHandler);
- button.click();
-
- await aTimeout(100);
-
- expect(submitHandler).to.not.have.been.called;
- });
});
});
diff --git a/src/components/radio-button/radio-button.ts b/src/components/radio-button/radio-button.ts
index 3acd6bf5..e32078dd 100644
--- a/src/components/radio-button/radio-button.ts
+++ b/src/components/radio-button/radio-button.ts
@@ -1,13 +1,10 @@
-import { LitElement } from 'lit';
-import { customElement, property, query, state } from 'lit/decorators.js';
+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 { 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';
+import styles from '~/components/button/button.styles';
+import RadioBase from '~/internal/radio';
+import { HasSlotController } from '~/internal/slot';
/**
* @since 2.0
@@ -24,109 +21,16 @@ import styles from './radio-button.styles';
* @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 LitElement {
+export default class SlRadioButton extends RadioBase {
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';
@@ -134,54 +38,57 @@ export default class SlRadioButton extends LitElement {
/** 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-group/radio-group.styles.ts b/src/components/radio-group/radio-group.styles.ts
index ffad5702..d6d7b579 100644
--- a/src/components/radio-group/radio-group.styles.ts
+++ b/src/components/radio-group/radio-group.styles.ts
@@ -1,5 +1,5 @@
import { css } from 'lit';
-import componentStyles from '../../styles/component.styles';
+import componentStyles from '~/styles/component.styles';
export default css`
${componentStyles}
diff --git a/src/components/radio-group/radio-group.ts b/src/components/radio-group/radio-group.ts
index dada60c1..7431f24d 100644
--- a/src/components/radio-group/radio-group.ts
+++ b/src/components/radio-group/radio-group.ts
@@ -1,9 +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 '../../components/button-group/button-group';
+import '~/components/button-group/button-group';
+import type SlRadio from '~/components/radio/radio';
import styles from './radio-group.styles';
-import type SlRadio from '../../components/radio/radio';
const RADIO_CHILDREN = ['sl-radio', 'sl-radio-button'];
diff --git a/src/components/radio/radio.styles.ts b/src/components/radio/radio.styles.ts
index cc5bf5f1..190313e2 100644
--- a/src/components/radio/radio.styles.ts
+++ b/src/components/radio/radio.styles.ts
@@ -1,6 +1,6 @@
import { css } from 'lit';
-import { focusVisibleSelector } from '../../internal/focus-visible';
-import componentStyles from '../../styles/component.styles';
+import { focusVisibleSelector } from '~/internal/focus-visible';
+import componentStyles from '~/styles/component.styles';
export default css`
${componentStyles}
diff --git a/src/components/radio/radio.test.ts b/src/components/radio/radio.test.ts
index 0654efbd..12c86e63 100644
--- a/src/components/radio/radio.test.ts
+++ b/src/components/radio/radio.test.ts
@@ -1,7 +1,7 @@
-import { aTimeout, expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
+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 SlRadioGroup from '~/components/radio-group/radio-group';
import type SlRadio from './radio';
describe('', () => {
@@ -75,7 +75,7 @@ describe('', () => {
-
+ Submit
@@ -96,29 +96,5 @@ describe('', () => {
expect(formData!.get('a')).to.equal('2');
});
-
- it('should show a constraint validation error when setCustomValidity() is called', async () => {
- const form = await fixture(html`
-
-
-
-
-
- Submit
-
- `);
- const button = form.querySelector('sl-button')!;
- const radio = form.querySelectorAll('sl-radio')[1]!;
- const submitHandler = sinon.spy((event: SubmitEvent) => event.preventDefault());
-
- // Submitting the form after setting custom validity should not trigger the handler
- radio.setCustomValidity('Invalid selection');
- form.addEventListener('submit', submitHandler);
- button.click();
-
- await aTimeout(100);
-
- expect(submitHandler).to.not.have.been.called;
- });
});
});
diff --git a/src/components/radio/radio.ts b/src/components/radio/radio.ts
index 26eecd38..6387da42 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';
-import { watch } from '../../internal/watch';
+import RadioBase from '~/internal/radio';
import styles from './radio.styles';
/**
@@ -24,102 +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('.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`