add disabled to radio group; remove @watch

This commit is contained in:
Cory LaViska
2025-06-23 14:35:14 -04:00
parent c11d6129a3
commit 3d142e2303
4 changed files with 86 additions and 22 deletions

View File

@@ -44,7 +44,7 @@ Set the `appearance` attribute to `button` on all radios to render a radio butto
<wa-radio appearance="button" value="3">Option 3</wa-radio>
</wa-radio-group>
<br>
<br />
<wa-radio-group
label="Vertical options"
@@ -60,12 +60,22 @@ Set the `appearance` attribute to `button` on all radios to render a radio butto
</wa-radio-group>
```
### Disabling Options
### Disabling
Radios and radio buttons can be disabled by adding the `disabled` attribute to the respective options inside the radio group.
To disable the entire radio group, add the `disabled` attribute to the radio group.
```html {.example}
<wa-radio-group label="Select an option" name="a" value="1">
<wa-radio-group label="Select an option" disabled>
<wa-radio value="1">Option 1</wa-radio>
<wa-radio value="2" disabled>Option 2</wa-radio>
<wa-radio value="3">Option 3</wa-radio>
</wa-radio-group>
```
To disable individual options, add the `disabled` attribute to the respective options.
```html {.example}
<wa-radio-group label="Select an option">
<wa-radio value="1">Option 1</wa-radio>
<wa-radio value="2" disabled>Option 2</wa-radio>
<wa-radio value="3">Option 3</wa-radio>

View File

@@ -1,10 +1,10 @@
import type { PropertyValues } from 'lit';
import { html, isServer } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { uniqueId } from '../../internal/math.js';
import { HasSlotController } from '../../internal/slot.js';
import { RequiredValidator } from '../../internal/validators/required-validator.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-form-associated-element.js';
import formControlStyles from '../../styles/component/form-control.css';
import sizeStyles from '../../styles/utilities/size.css';
@@ -73,6 +73,9 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
/** The name of the radio group, submitted as a name/value pair with form data. */
@property({ reflect: true }) name: string | null = null;
/** Disables the radio group and all child radios. */
@property({ type: Boolean, reflect: true }) disabled = false;
/** The orientation in which to show radio items. */
@property({ reflect: true }) orientation: 'horizontal' | 'vertical' = 'vertical';
@@ -141,6 +144,12 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
return radio;
}
updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has('disabled') || changedProperties.has('value')) {
this.syncRadioElements();
}
}
formResetCallback(...args: Parameters<WebAwesomeFormAssociatedElement['formResetCallback']>) {
this.value = this.defaultValue;
@@ -152,7 +161,7 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
private handleRadioClick = (e: Event) => {
const clickedRadio = (e.target as HTMLElement).closest<WaRadio>('wa-radio');
if (!clickedRadio || clickedRadio.disabled) {
if (!clickedRadio || clickedRadio.disabled || (clickedRadio as any).forceDisabled || this.disabled) {
return;
}
@@ -199,6 +208,9 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
radio.toggleAttribute('data-wa-radio-first', index === 0);
radio.toggleAttribute('data-wa-radio-inner', index !== 0 && index !== radios.length - 1);
radio.toggleAttribute('data-wa-radio-last', index === radios.length - 1);
// Set forceDisabled state based on radio group's disabled state
(radio as WaRadio).forceDisabled = this.disabled;
});
// If at least one radio button exists, we assume it's a radio button group
@@ -216,18 +228,42 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
}),
);
if (radios.length > 0 && !radios.some(radio => radio.checked)) {
radios[0].setAttribute('tabindex', '0');
// Manage tabIndex based on disabled state and checked status
if (this.disabled) {
// If radio group is disabled, all radios should not be tabbable
radios.forEach(radio => {
radio.tabIndex = -1;
});
} else {
// Normal tabbing behavior
const enabledRadios = radios.filter(radio => !radio.disabled);
const checkedRadio = enabledRadios.find(radio => radio.checked);
if (enabledRadios.length > 0) {
if (checkedRadio) {
// If there's a checked radio, it should be tabbable
enabledRadios.forEach(radio => {
radio.tabIndex = radio.checked ? 0 : -1;
});
} else {
// If no radio is checked, first enabled radio should be tabbable
enabledRadios.forEach((radio, index) => {
radio.tabIndex = index === 0 ? 0 : -1;
});
}
}
// Disabled radios should never be tabbable
radios
.filter(radio => radio.disabled)
.forEach(radio => {
radio.tabIndex = -1;
});
}
}
@watch('value')
handleValueChange() {
this.syncRadioElements();
}
private handleKeyDown(event: KeyboardEvent) {
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(event.key)) {
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(event.key) || this.disabled) {
return;
}
@@ -287,6 +323,8 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
/** Sets focus on the radio group. */
public focus(options?: FocusOptions) {
if (this.disabled) return;
const radios = this.getAllRadios();
const checked = radios.find(radio => radio.checked);
const firstEnabledRadio = radios.find(radio => !radio.disabled);

View File

@@ -89,7 +89,7 @@
}
/* Disabled */
:host([disabled]) {
:host(:state(disabled)) {
opacity: 0.5;
cursor: not-allowed;
}
@@ -142,7 +142,7 @@
}
@media (hover: hover) {
:host([appearance='button']:hover:not([disabled], :state(checked))) {
:host([appearance='button']:hover:not(:state(disabled), :state(checked))) {
background-color: color-mix(in srgb, var(--wa-color-surface-default) 95%, var(--wa-color-mix-hover));
}
}

View File

@@ -44,6 +44,9 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
@state() checked = false;
/** @internal Used by radio group to force disable radios while preserving their original disabled state. */
@state() forceDisabled = false;
/**
* The string pointing to a form's id.
*/
@@ -79,7 +82,7 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
private setInitialAttributes() {
this.setAttribute('role', 'radio');
this.tabIndex = 0;
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
this.setAttribute('aria-disabled', this.disabled || this.forceDisabled ? 'true' : 'false');
}
updated(changedProperties: PropertyValues<this>) {
@@ -88,12 +91,24 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
if (changedProperties.has('checked')) {
this.customStates.set('checked', this.checked);
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
this.tabIndex = this.checked ? 0 : -1;
// Only set tabIndex if not disabled
if (!this.disabled && !this.forceDisabled) {
this.tabIndex = this.checked ? 0 : -1;
}
}
if (changedProperties.has('disabled')) {
this.customStates.set('disabled', this.disabled);
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
if (changedProperties.has('disabled') || changedProperties.has('forceDisabled')) {
const effectivelyDisabled = this.disabled || this.forceDisabled;
this.customStates.set('disabled', effectivelyDisabled);
this.setAttribute('aria-disabled', effectivelyDisabled ? 'true' : 'false');
// Set tabIndex based on disabled state
if (effectivelyDisabled) {
this.tabIndex = -1;
} else {
// Restore proper tabIndex - this will be managed by the radio group
this.tabIndex = this.checked ? 0 : -1;
}
}
}
@@ -104,8 +119,9 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
// We override `setValue` because we don't want to set form values from here. We want to do that in "RadioGroup" itself.
}
// Update the handleClick method (around line 75)
private handleClick = () => {
if (!this.disabled) {
if (!this.disabled && !this.forceDisabled) {
this.checked = true;
}
};