From 0a5fb5e9e7a05f4c545ba9f2558af6b0ee3f103f Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Wed, 23 Mar 2022 17:37:24 -0400 Subject: [PATCH] refactor radio base class --- docs/resources/changelog.md | 8 + src/components/button/button.styles.ts | 5 +- .../radio-button/radio-button.styles.ts | 25 +++ src/components/radio-button/radio-button.ts | 193 +++++++++++++----- src/components/radio/radio.ts | 103 +++++++++- src/internal/radio.ts | 108 ---------- 6 files changed, 278 insertions(+), 164 deletions(-) create mode 100644 src/components/radio-button/radio-button.styles.ts delete mode 100644 src/internal/radio.ts diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index 49568622..e7830de6 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -6,6 +6,14 @@ Components with the Experimental bad _During the beta period, these restrictions may be relaxed in the event of a mission-critical bug._ 🐛 +## 2.0.0-beta.73 + +- Added `button` part to `` +- Added custom validity examples and tests to ``, ``, and `` +- Fixed a bug that prevented `setCustomValidity()` from working with `` +- Fixed a bug where the right border of a checked `` was the wrong color +- Once again removed path aliasing because it doesn't work with Web Test Runner's esbuild plugin + ## 2.0.0-beta.72 - 🚨 BREAKING: refactored parts in ``, ``, ``, and `` 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`