mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 04:09:12 +00:00
fixes #965
This commit is contained in:
@@ -57,9 +57,14 @@ export default class SlRadioButton extends ShoelaceElement {
|
||||
this.setAttribute('role', 'presentation');
|
||||
}
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
||||
/** Sets focus on the button. */
|
||||
focus(options?: FocusOptions) {
|
||||
this.input.focus(options);
|
||||
}
|
||||
|
||||
/** Removes focus from the button. */
|
||||
blur() {
|
||||
this.input.blur();
|
||||
}
|
||||
|
||||
handleBlur() {
|
||||
@@ -77,6 +82,11 @@ export default class SlRadioButton extends ShoelaceElement {
|
||||
this.checked = true;
|
||||
}
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
||||
}
|
||||
|
||||
handleFocus() {
|
||||
this.hasFocus = true;
|
||||
this.emit('sl-focus');
|
||||
|
||||
@@ -1,47 +1,22 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import formControlStyles from '../../styles/form-control.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
${formControlStyles}
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
border: solid var(--sl-panel-border-width) var(--sl-panel-border-color);
|
||||
border-radius: var(--sl-border-radius-medium);
|
||||
padding: var(--sl-spacing-large);
|
||||
padding-top: var(--sl-spacing-x-small);
|
||||
}
|
||||
|
||||
.radio-group .radio-group__label {
|
||||
font-family: var(--sl-input-font-family);
|
||||
font-size: var(--sl-input-font-size-medium);
|
||||
font-weight: var(--sl-input-font-weight);
|
||||
color: var(--sl-input-color);
|
||||
padding: 0 var(--sl-spacing-2x-small);
|
||||
}
|
||||
|
||||
::slotted(sl-radio:not(:last-of-type)) {
|
||||
margin-bottom: var(--sl-spacing-2x-small);
|
||||
}
|
||||
|
||||
.radio-group:not(.radio-group--has-fieldset) {
|
||||
.form-control {
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.radio-group:not(.radio-group--has-fieldset) .radio-group__label {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
.form-control__label {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.radio-group--required .radio-group__label::after {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { FormSubmitController } from '../../internal/form';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { watch } from '../../internal/watch';
|
||||
import '../button-group/button-group';
|
||||
import styles from './radio-group.styles';
|
||||
@@ -23,8 +24,10 @@ import type { CSSResultGroup } from 'lit';
|
||||
*
|
||||
* @event sl-change - Emitted when the radio group's selected value changes.
|
||||
*
|
||||
* @csspart base - The component's internal wrapper.
|
||||
* @csspart label - The radio group's label.
|
||||
* @csspart form-control - The form control that wraps the label, input, and help-text.
|
||||
* @csspart form-control-label - The label's wrapper.
|
||||
* @csspart form-control-input - The input's wrapper.
|
||||
* @csspart form-control-help-text - The help text's wrapper.
|
||||
* @csspart button-group - The button group that wraps radio buttons.
|
||||
* @csspart button-group__base - The button group's `base` part.
|
||||
*/
|
||||
@@ -35,6 +38,7 @@ export default class SlRadioGroup extends ShoelaceElement {
|
||||
protected readonly formSubmitController = new FormSubmitController(this, {
|
||||
defaultValue: (control: SlRadioGroup) => control.defaultValue
|
||||
});
|
||||
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||
|
||||
@query('slot:not([name])') defaultSlot: HTMLSlotElement;
|
||||
@query('.radio-group__validation-input') input: HTMLInputElement;
|
||||
@@ -50,6 +54,9 @@ export default class SlRadioGroup extends ShoelaceElement {
|
||||
*/
|
||||
@property() label = '';
|
||||
|
||||
/** The input's help text. If you need to display HTML, you can use the `help-text` slot instead. */
|
||||
@property({ attribute: 'help-text' }) helpText = '';
|
||||
|
||||
/** The selected value of the control. */
|
||||
@property({ reflect: true }) value = '';
|
||||
|
||||
@@ -62,9 +69,6 @@ export default class SlRadioGroup extends ShoelaceElement {
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) invalid = false;
|
||||
|
||||
/** Shows the fieldset and legend that surrounds the radio group. */
|
||||
@property({ type: Boolean, attribute: 'fieldset', reflect: true }) fieldset = false;
|
||||
|
||||
/** Ensures a child radio is checked before allowing the containing form to submit. */
|
||||
@property({ type: Boolean, reflect: true }) required = false;
|
||||
|
||||
@@ -127,11 +131,11 @@ export default class SlRadioGroup extends ShoelaceElement {
|
||||
return !this.invalid;
|
||||
}
|
||||
|
||||
private getAllRadios() {
|
||||
getAllRadios() {
|
||||
return [...this.querySelectorAll<SlRadio | SlRadioButton>('sl-radio, sl-radio-button')];
|
||||
}
|
||||
|
||||
private handleRadioClick(event: MouseEvent) {
|
||||
handleRadioClick(event: MouseEvent) {
|
||||
const target = event.target as SlRadio | SlRadioButton;
|
||||
|
||||
if (target.disabled) {
|
||||
@@ -143,7 +147,7 @@ export default class SlRadioGroup extends ShoelaceElement {
|
||||
radios.forEach(radio => (radio.checked = radio === target));
|
||||
}
|
||||
|
||||
private handleKeyDown(event: KeyboardEvent) {
|
||||
handleKeyDown(event: KeyboardEvent) {
|
||||
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(event.key)) {
|
||||
return;
|
||||
}
|
||||
@@ -180,7 +184,18 @@ export default class SlRadioGroup extends ShoelaceElement {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
private handleSlotChange() {
|
||||
handleLabelClick() {
|
||||
const radios = this.getAllRadios();
|
||||
const checked = radios.find(radio => radio.checked);
|
||||
const radioToFocus = checked || radios[0];
|
||||
|
||||
// Move focus to the checked radio (or the first one if none are checked) when clicking the label
|
||||
if (radioToFocus) {
|
||||
radioToFocus.focus();
|
||||
}
|
||||
}
|
||||
|
||||
handleSlotChange() {
|
||||
const radios = this.getAllRadios();
|
||||
|
||||
radios.forEach(radio => (radio.checked = radio.value === this.value));
|
||||
@@ -205,18 +220,23 @@ export default class SlRadioGroup extends ShoelaceElement {
|
||||
}
|
||||
}
|
||||
|
||||
private showNativeErrorMessage() {
|
||||
showNativeErrorMessage() {
|
||||
this.input.hidden = false;
|
||||
this.input.reportValidity();
|
||||
setTimeout(() => (this.input.hidden = true), 10000);
|
||||
}
|
||||
|
||||
private updateCheckedRadio() {
|
||||
updateCheckedRadio() {
|
||||
const radios = this.getAllRadios();
|
||||
radios.forEach(radio => (radio.checked = radio.value === this.value));
|
||||
}
|
||||
|
||||
render() {
|
||||
const hasLabelSlot = this.hasSlotController.test('label');
|
||||
const hasHelpTextSlot = this.hasSlotController.test('help-text');
|
||||
const hasLabel = this.label ? true : !!hasLabelSlot;
|
||||
const hasHelpText = this.helpText ? true : !!hasHelpTextSlot;
|
||||
|
||||
const defaultSlot = html`
|
||||
<slot
|
||||
@click=${this.handleRadioClick}
|
||||
@@ -228,34 +248,63 @@ export default class SlRadioGroup extends ShoelaceElement {
|
||||
|
||||
return html`
|
||||
<fieldset
|
||||
part="base"
|
||||
role="radiogroup"
|
||||
aria-errormessage="radio-error-message"
|
||||
aria-invalid="${this.invalid}"
|
||||
part="form-control"
|
||||
class=${classMap({
|
||||
'radio-group': true,
|
||||
'radio-group--has-fieldset': this.fieldset,
|
||||
'radio-group--required': this.required
|
||||
'form-control': true,
|
||||
'form-control--medium': true,
|
||||
'form-control--radio-group': true,
|
||||
'form-control--has-label': hasLabel,
|
||||
'form-control--has-help-text': hasHelpText
|
||||
})}
|
||||
role="radiogroup"
|
||||
aria-labelledby="label"
|
||||
aria-describedby="help-text"
|
||||
aria-errormessage="error-message"
|
||||
>
|
||||
<legend part="label" class="radio-group__label">
|
||||
<label
|
||||
part="form-control-label"
|
||||
id="label"
|
||||
class="form-control__label"
|
||||
aria-hidden=${hasLabel ? 'false' : 'true'}
|
||||
@click=${this.handleLabelClick}
|
||||
>
|
||||
<slot name="label">${this.label}</slot>
|
||||
</legend>
|
||||
<div class="visually-hidden">
|
||||
<div id="radio-error-message" aria-live="assertive">${this.errorMessage}</div>
|
||||
<label class="radio-group__validation visually-hidden">
|
||||
<input type="text" class="radio-group__validation-input" ?required=${this.required} tabindex="-1" hidden />
|
||||
</label>
|
||||
</label>
|
||||
|
||||
<div part="form-control-input" class="form-control-input">
|
||||
<div class="visually-hidden">
|
||||
<div id="error-message" aria-live="assertive">${this.errorMessage}</div>
|
||||
<label class="radio-group__validation">
|
||||
<input
|
||||
type="text"
|
||||
class="radio-group__validation-input"
|
||||
?required=${this.required}
|
||||
tabindex="-1"
|
||||
hidden
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
${this.hasButtonGroup
|
||||
? html`
|
||||
<sl-button-group part="button-group" exportparts="base:button-group__base">
|
||||
${defaultSlot}
|
||||
</sl-button-group>
|
||||
`
|
||||
: defaultSlot}
|
||||
</div>
|
||||
|
||||
<div
|
||||
part="form-control-help-text"
|
||||
id="help-text"
|
||||
class="form-control__help-text"
|
||||
aria-hidden=${hasHelpText ? 'false' : 'true'}
|
||||
>
|
||||
<slot name="help-text">${this.helpText}</slot>
|
||||
</div>
|
||||
${this.hasButtonGroup
|
||||
? html`
|
||||
<sl-button-group part="button-group" exportparts="base:button-group__base">
|
||||
${defaultSlot}
|
||||
</sl-button-group>
|
||||
`
|
||||
: defaultSlot}
|
||||
</fieldset>
|
||||
`;
|
||||
/* eslint-enable lit-a11y/click-events-have-key-events */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,4 +55,8 @@ export default css`
|
||||
.form-control--has-help-text.form-control--large .form-control__help-text {
|
||||
font-size: var(--sl-input-help-text-font-size-large);
|
||||
}
|
||||
|
||||
.form-control--has-help-text.form-control--radio-group .form-control__help-text {
|
||||
margin-top: var(--sl-spacing-2x-small);
|
||||
}
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user