Skip to content

Commit 2c9fa25

Browse files
committed
refactor(multiple): default softDisabled to true (angular#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. (cherry picked from commit 28a50f5)
1 parent 56631cb commit 2c9fa25

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)