refactor radio base class

pull/721/head
Cory LaViska 2022-03-23 17:37:24 -04:00
rodzic 99f475b56f
commit 0a5fb5e9e7
6 zmienionych plików z 278 dodań i 164 usunięć

Wyświetl plik

@ -6,6 +6,14 @@ Components with the <sl-badge variant="warning" pill>Experimental</sl-badge> 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 `<sl-radio-button>`
- Added custom validity examples and tests to `<sl-checkbox>`, `<sl-radio>`, and `<sl-radio-button>`
- Fixed a bug that prevented `setCustomValidity()` from working with `<sl-radio-button>`
- Fixed a bug where the right border of a checked `<sl-radio-button>` 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 `<sl-input>`, `<sl-range>`, `<sl-select>`, and `<sl-textarea>` to allow you to customize the label and help text position

Wyświetl plik

@ -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;
}
`;

Wyświetl plik

@ -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;
}
`;

Wyświetl plik

@ -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`
<button
part="base"
class=${classMap({
button: true,
'button--default': this.variant === 'default',
'button--primary': this.variant === 'primary',
'button--success': this.variant === 'success',
'button--neutral': this.variant === 'neutral',
'button--warning': this.variant === 'warning',
'button--danger': this.variant === 'danger',
'button--small': this.size === 'small',
'button--medium': this.size === 'medium',
'button--large': this.size === 'large',
'button--checked': this.checked,
'button--disabled': this.disabled,
'button--focused': this.hasFocus,
'button--outline': true,
'button--pill': this.pill,
'button--has-label': this.hasSlotController.test('[default]'),
'button--has-prefix': this.hasSlotController.test('prefix'),
'button--has-suffix': this.hasSlotController.test('suffix')
})}
?disabled=${this.disabled}
type="button"
name=${ifDefined(this.name)}
value=${ifDefined(this.value)}
@blur=${this.handleBlur}
@focus=${this.handleFocus}
@click=${this.handleClick}
>
<span part="prefix" class="button__prefix">
<slot name="prefix"></slot>
</span>
<span part="label" class="button__label">
<slot></slot>
</span>
<span part="suffix" class="button__suffix">
<slot name="suffix"></slot>
</span>
</button>
<div part="base">
<input class="hidden-input" type="radio" aria-hidden="true" tabindex="-1" />
<button
part="button"
class=${classMap({
button: true,
'button--default': this.variant === 'default',
'button--primary': this.variant === 'primary',
'button--success': this.variant === 'success',
'button--neutral': this.variant === 'neutral',
'button--warning': this.variant === 'warning',
'button--danger': this.variant === 'danger',
'button--small': this.size === 'small',
'button--medium': this.size === 'medium',
'button--large': this.size === 'large',
'button--checked': this.checked,
'button--disabled': this.disabled,
'button--focused': this.hasFocus,
'button--outline': true,
'button--pill': this.pill,
'button--has-label': this.hasSlotController.test('[default]'),
'button--has-prefix': this.hasSlotController.test('prefix'),
'button--has-suffix': this.hasSlotController.test('suffix')
})}
?disabled=${this.disabled}
type="button"
name=${ifDefined(this.name)}
value=${ifDefined(this.value)}
@blur=${this.handleBlur}
@focus=${this.handleFocus}
@click=${this.handleClick}
>
<span part="prefix" class="button__prefix">
<slot name="prefix"></slot>
</span>
<span part="label" class="button__label">
<slot></slot>
</span>
<span part="suffix" class="button__suffix">
<slot name="suffix"></slot>
</span>
</button>
</div>
`;
}
}

Wyświetl plik

@ -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`
<label

Wyświetl plik

@ -1,108 +0,0 @@
import { LitElement } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { emit } from '../internal/event';
import { FormSubmitController } from '../internal/form';
import { watch } from '../internal/watch';
/**
* The following events are emitted by the base class. When extending, these comments should be prepended to the
* component so they show up in its documentation.
*
* @event sl-blur - Emitted when the control loses focus.
* @event sl-change - Emitted when the control's checked state changes.
* @event sl-focus - Emitted when the control gains focus.
*/
export default abstract class RadioBase extends LitElement {
@query('input[type="radio"], button') input: HTMLInputElement;
protected readonly formSubmitController = new FormSubmitController(this, {
value: (control: RadioBase) => (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();
}
}
}