This commit is contained in:
Cory LaViska
2022-11-09 15:27:51 -05:00
parent 0d9767596a
commit f03b09a410
6 changed files with 112 additions and 71 deletions

View File

@@ -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');

View File

@@ -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 {

View File

@@ -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 */
}
}

View File

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