kopia lustrzana https://github.com/shoelace-style/shoelace
refactor radio base class
rodzic
99f475b56f
commit
0a5fb5e9e7
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
`;
|
|
@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
Ładowanie…
Reference in New Issue