diff --git a/packages/webawesome/docs/docs/components/radio-group.md b/packages/webawesome/docs/docs/components/radio-group.md index 9fa82ee04..b628a393b 100644 --- a/packages/webawesome/docs/docs/components/radio-group.md +++ b/packages/webawesome/docs/docs/components/radio-group.md @@ -44,7 +44,7 @@ Set the `appearance` attribute to `button` on all radios to render a radio butto Option 3 -
+
``` -### 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} - + + Option 1 + Option 2 + Option 3 + +``` + +To disable individual options, add the `disabled` attribute to the respective options. + +```html {.example} + Option 1 Option 2 Option 3 diff --git a/packages/webawesome/src/components/radio-group/radio-group.ts b/packages/webawesome/src/components/radio-group/radio-group.ts index b99ccb53f..0a2cba15f 100644 --- a/packages/webawesome/src/components/radio-group/radio-group.ts +++ b/packages/webawesome/src/components/radio-group/radio-group.ts @@ -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) { + if (changedProperties.has('disabled') || changedProperties.has('value')) { + this.syncRadioElements(); + } + } + formResetCallback(...args: Parameters) { 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('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); diff --git a/packages/webawesome/src/components/radio/radio.css b/packages/webawesome/src/components/radio/radio.css index 827c18195..12ab57deb 100644 --- a/packages/webawesome/src/components/radio/radio.css +++ b/packages/webawesome/src/components/radio/radio.css @@ -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)); } } diff --git a/packages/webawesome/src/components/radio/radio.ts b/packages/webawesome/src/components/radio/radio.ts index cdec1799a..a6dca2ea5 100644 --- a/packages/webawesome/src/components/radio/radio.ts +++ b/packages/webawesome/src/components/radio/radio.ts @@ -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) { @@ -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; } };