Skip to content

Commit 28a50f5

Browse files
authored
refactor(multiple): default softDisabled to true (#32240)
* refactor(multiple): default softDisabled to true Updates the default value of the input to across all ARIA components. This allows disabled items to receive focus by default, improving keyboard accessibility. - Grid focus coordinates behavior has also been updated to correctly handle focus when is enabled. * fix(cdk/a11y): update tests to reflect correct behavior * refactor(cdk/a11y): refine softDisabled behavior and update tests\n\nThis commit refines the behavior across several a11y components (List, Listbox, Accordion, Tree) to ensure correct interaction between disabled states and navigation/selection. Specifically:\n\n- In , the method now explicitly checks if the list is disabled before allowing selection updates, preventing unintended selections when permits navigation.\n- In , a new method is introduced to clearly distinguish between a 'hard' disabled state (blocking all interaction) and a 'soft' disabled state (allowing navigation but blocking selection).\n- Corresponding tests in , , , and have been updated and expanded to accurately reflect and verify these refined interactions, ensuring that navigation and selection behave as expected in various disabled scenarios.
1 parent a9bd33f commit 28a50f5

File tree

26 files changed

+343
-96
lines changed

26 files changed

+343
-96
lines changed

src/aria/accordion/accordion.spec.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,12 +363,19 @@ describe('AccordionGroup', () => {
363363
});
364364

365365
it('should not allow keyboard navigation if group is disabled', () => {
366-
configureAccordionComponent({disabledGroup: true});
366+
configureAccordionComponent({disabledGroup: true, softDisabled: false});
367367

368368
downArrowKey(triggerElements[0]);
369369
expect(isTriggerActive(triggerElements[1])).toBeFalse();
370370
});
371371

372+
it('should allow keyboard navigation if group is disabled', () => {
373+
configureAccordionComponent({disabledGroup: true});
374+
375+
downArrowKey(triggerElements[0]);
376+
expect(isTriggerActive(triggerElements[1])).toBeTrue();
377+
});
378+
372379
it('should not allow expansion if group is disabled', () => {
373380
configureAccordionComponent({disabledGroup: true});
374381

@@ -419,7 +426,7 @@ class AccordionGroupExample {
419426
value = model<string[]>([]);
420427
multiExpandable = signal(false);
421428
disabledGroup = signal(false);
422-
softDisabled = signal(false);
429+
softDisabled = signal(true);
423430
wrap = signal(false);
424431

425432
disableItem(itemValue: string, disabled: boolean) {

src/aria/accordion/accordion.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ export class AccordionGroup {
172172
value = model<string[]>([]);
173173

174174
/** Whether to allow disabled items to receive focus. */
175-
softDisabled = input(false, {transform: booleanAttribute});
175+
softDisabled = input(true, {transform: booleanAttribute});
176176

177177
/** Whether keyboard navigation should wrap around from the last item to the first, and vice-versa. */
178178
wrap = input(false, {transform: booleanAttribute});

src/aria/grid/grid.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export class Grid {
6666
readonly disabled = input(false, {transform: booleanAttribute});
6767

6868
/** Whether to allow disabled items to receive focus. */
69-
readonly softDisabled = input(false, {transform: booleanAttribute});
69+
readonly softDisabled = input(true, {transform: booleanAttribute});
7070

7171
/** The focus strategy used by the grid. */
7272
readonly focusMode = input<'roving' | 'activedescendant'>('roving');

src/aria/listbox/listbox.spec.ts

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -195,11 +195,16 @@ describe('Listbox', () => {
195195
expect(listboxElement.getAttribute('tabindex')).toBe('-1');
196196
});
197197

198-
it('should set tabindex="0" for the listbox when disabled and focusMode is "roving"', () => {
199-
setupListbox({disabled: true, focusMode: 'roving'});
198+
it('should set tabindex="0" for the listbox when disabled and focusMode is "roving when softDisabled is false"', () => {
199+
setupListbox({disabled: true, focusMode: 'roving', softDisabled: false});
200200
expect(listboxElement.getAttribute('tabindex')).toBe('0');
201201
});
202202

203+
it('should set tabindex="-1" for the listbox when disabled and focusMode is "roving"', () => {
204+
setupListbox({disabled: true, focusMode: 'roving'});
205+
expect(listboxElement.getAttribute('tabindex')).toBe('-1');
206+
});
207+
203208
it('should set initial focus (tabindex="0") on the first non-disabled option if no value is set', () => {
204209
setupListbox({focusMode: 'roving'});
205210
expect(optionElements[0].getAttribute('tabindex')).toBe('0');
@@ -218,8 +223,23 @@ describe('Listbox', () => {
218223
expect(optionElements[4].getAttribute('tabindex')).toBe('-1');
219224
});
220225

221-
it('should set initial focus (tabindex="0") on the first non-disabled option if selected option is disabled', () => {
222-
setupListbox({focusMode: 'roving', value: [1], disabledOptions: [1]});
226+
it('should set initial focus (tabindex="0") on the first non-disabled option if selected option is disabled when softDisabled is false', () => {
227+
setupListbox({
228+
focusMode: 'roving',
229+
value: [1],
230+
disabledOptions: [0],
231+
softDisabled: false,
232+
});
233+
expect(optionElements[0].getAttribute('tabindex')).toBe('-1');
234+
expect(optionElements[1].getAttribute('tabindex')).toBe('0');
235+
});
236+
237+
it('should set initial focus (tabindex="0") on the first option if selected option is disabled', () => {
238+
setupListbox({
239+
focusMode: 'roving',
240+
value: [0],
241+
disabledOptions: [0],
242+
});
223243
expect(optionElements[0].getAttribute('tabindex')).toBe('0');
224244
expect(optionElements[1].getAttribute('tabindex')).toBe('-1');
225245
});
@@ -247,10 +267,20 @@ describe('Listbox', () => {
247267
});
248268

249269
it('should set aria-activedescendant to the ID of the first non-disabled option if selected option is disabled', () => {
250-
setupListbox({focusMode: 'activedescendant', value: [1], disabledOptions: [1]});
270+
setupListbox({focusMode: 'activedescendant', value: [0], disabledOptions: [0]});
251271
expect(listboxElement.getAttribute('aria-activedescendant')).toBe(optionElements[0].id);
252272
});
253273

274+
it('should set aria-activedescendant to the ID of the first non-disabled option if selected option is disabled when softDisabled is false', () => {
275+
setupListbox({
276+
focusMode: 'activedescendant',
277+
value: [1],
278+
disabledOptions: [0],
279+
softDisabled: false,
280+
});
281+
expect(listboxElement.getAttribute('aria-activedescendant')).toBe(optionElements[1].id);
282+
});
283+
254284
it('should set tabindex="-1" for all options', () => {
255285
setupListbox({focusMode: 'activedescendant'});
256286
expect(optionElements[0].getAttribute('tabindex')).toBe('-1');
@@ -553,17 +583,17 @@ describe('Listbox', () => {
553583
isFocused: (index: number) => boolean,
554584
) {
555585
describe(`keyboard navigation (focusMode="${focusMode}")`, () => {
556-
it('should move focus to the last enabled option on End', () => {
586+
it('should move focus to the last focusable option on End', () => {
557587
setupListbox({focusMode, disabledOptions: [4]});
558588
end();
559-
expect(isFocused(3)).toBe(true);
589+
expect(isFocused(4)).toBe(true);
560590
});
561591

562-
it('should move focus to the first enabled option on Home', () => {
592+
it('should move focus to the first focusable option on Home', () => {
563593
setupListbox({focusMode, disabledOptions: [0]});
564594
end();
565595
home();
566-
expect(isFocused(1)).toBe(true);
596+
expect(isFocused(0)).toBe(true);
567597
});
568598

569599
it('should allow keyboard navigation if the group is readonly', () => {
@@ -614,6 +644,18 @@ describe('Listbox', () => {
614644
down();
615645
expect(isFocused(1)).toBe(true);
616646
});
647+
648+
it('should not skip disabled options with ArrowDown when completely disabled', () => {
649+
setupListbox({
650+
focusMode,
651+
orientation: 'vertical',
652+
softDisabled: true,
653+
disabled: true,
654+
});
655+
656+
down();
657+
expect(isFocused(0)).toBe(true);
658+
});
617659
});
618660

619661
describe('horizontal orientation', () => {
@@ -774,7 +816,7 @@ class ListboxExample {
774816
value: number[] = [];
775817
disabled = false;
776818
readonly = false;
777-
softDisabled = false;
819+
softDisabled = true;
778820
focusMode: 'roving' | 'activedescendant' = 'roving';
779821
orientation: 'vertical' | 'horizontal' = 'vertical';
780822
multi = false;

src/aria/listbox/listbox.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export class Listbox<V> {
9898
wrap = input(true, {transform: booleanAttribute});
9999

100100
/** Whether to allow disabled items in the list to receive focus. */
101-
softDisabled = input(false, {transform: booleanAttribute});
101+
softDisabled = input(true, {transform: booleanAttribute});
102102

103103
/** The focus strategy used by the list. */
104104
focusMode = input<'roving' | 'activedescendant'>('roving');

src/aria/private/accordion/accordion.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ describe('Accordion Pattern', () => {
6666
multiExpandable: signal(true),
6767
items: signal([]),
6868
expandedIds: signal<string[]>([]),
69-
softDisabled: signal(false),
69+
softDisabled: signal(true),
7070
wrap: signal(true),
7171
element: signal(document.createElement('div')),
7272
};

src/aria/private/behaviors/grid/grid-focus.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export class GridFocus<T extends GridFocusCell> {
143143

144144
/** Moves focus to the cell at the given coordinates if it's part of a focusable cell. */
145145
focusCoordinates(coords: RowCol): boolean {
146-
if (this.gridDisabled()) {
146+
if (this.gridDisabled() && !this.inputs.softDisabled()) {
147147
return false;
148148
}
149149

0 commit comments

Comments
 (0)