Skip to content

Commit 0ecd962

Browse files
committed
fix(material/core): remove tabindex from mat-option
Remove the tabindex attribute added to MatOption components. MatOption does not need tabindex because the parent component manages focus by setting `aria-activedescendant` attribute. Previously, MatOption set tabindex but was also a referenced by aria-activedescendant. This was not the correct ARIA semantics. Align closer to ARIA spec by using only aria-activedescendant rather than both. Tabindex="-1" seems to be causing a problem in #26861 where VoiceOver with Firefox moves DOM focus from the combobox to the option when opening the listbox popup. Unblocks #26861.
1 parent df839e6 commit 0ecd962

File tree

6 files changed

+26
-17
lines changed

6 files changed

+26
-17
lines changed

src/material/core/option/option.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -204,11 +204,6 @@ export class _MatOptionBase<T = any> implements FocusableOption, AfterViewChecke
204204
}
205205
}
206206

207-
/** Returns the correct tabindex for the option depending on disabled state. */
208-
_getTabIndex(): string {
209-
return this.disabled ? '-1' : '0';
210-
}
211-
212207
/** Gets the host DOM element. */
213208
_getHostElement(): HTMLElement {
214209
return this._element.nativeElement;
@@ -251,7 +246,6 @@ export class _MatOptionBase<T = any> implements FocusableOption, AfterViewChecke
251246
exportAs: 'matOption',
252247
host: {
253248
'role': 'option',
254-
'[attr.tabindex]': '_getTabIndex()',
255249
'[class.mdc-list-item--selected]': 'selected',
256250
'[class.mat-mdc-option-multiple]': 'multiple',
257251
'[class.mat-mdc-option-active]': 'active',

src/material/legacy-core/option/option.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,9 @@ export class MatLegacyOption<T = any> extends _MatOptionBase<T> {
5959
) {
6060
super(element, changeDetectorRef, parent, group);
6161
}
62+
63+
/** Returns the correct tabindex for the option depending on disabled state. */
64+
_getTabIndex(): string {
65+
return this.disabled ? '-1' : '0';
66+
}
6267
}

src/material/legacy-select/select.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ export class MatLegacySelect extends _MatSelectBase<MatLegacySelectChange> imple
489489
selectedOptionOffset = 0;
490490
} else {
491491
selectedOptionOffset = Math.max(
492-
this.options.toArray().indexOf(this._selectionModel.selected[0]),
492+
this.options.toArray().indexOf(this._selectionModel.selected[0] as any),
493493
0,
494494
);
495495
}

src/material/select/select.spec.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -824,17 +824,27 @@ describe('MDC-based MatSelect', () => {
824824
'mat-option',
825825
) as NodeListOf<HTMLElement>;
826826

827-
options[3].focus();
827+
select.focus();
828+
multiFixture.detectChanges();
829+
multiFixture.componentInstance.select._keyManager.setActiveItem(3);
830+
multiFixture.detectChanges();
831+
828832
expect(document.activeElement)
829-
.withContext('Expected fourth option to be focused.')
830-
.toBe(options[3]);
833+
.withContext('Expected select to have DOM focus.')
834+
.toBe(select);
835+
expect(select.getAttribute('aria-activedescendant'))
836+
.withContext('Expected fourth option to be activated.')
837+
.toBe(options[3].id);
831838

832839
multiFixture.componentInstance.control.setValue(['steak-0', 'sushi-7']);
833840
multiFixture.detectChanges();
834841

835842
expect(document.activeElement)
836-
.withContext('Expected fourth option to remain focused.')
837-
.toBe(options[3]);
843+
.withContext('Expected select to have DOM focus.')
844+
.toBe(select);
845+
expect(select.getAttribute('aria-activedescendant'))
846+
.withContext('Expected fourth optino to remain activated.')
847+
.toBe(options[3].id);
838848
}),
839849
);
840850

@@ -1260,10 +1270,10 @@ describe('MDC-based MatSelect', () => {
12601270
.toBe(true);
12611271
}));
12621272

1263-
it('should set the tabindex of each option according to disabled state', fakeAsync(() => {
1264-
expect(options[0].getAttribute('tabindex')).toEqual('0');
1265-
expect(options[1].getAttribute('tabindex')).toEqual('0');
1266-
expect(options[2].getAttribute('tabindex')).toEqual('-1');
1273+
it('should omit the tabindex attribute on each option', fakeAsync(() => {
1274+
expect(options[0].hasAttribute('tabindex')).toBeFalse();
1275+
expect(options[1].hasAttribute('tabindex')).toBeFalse();
1276+
expect(options[2].hasAttribute('tabindex')).toBeFalse();
12671277
}));
12681278

12691279
it('should set aria-disabled for disabled options', fakeAsync(() => {

tools/public_api_guard/material/core.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,6 @@ export class _MatOptionBase<T = any> implements FocusableOption, AfterViewChecke
281281
focus(_origin?: FocusOrigin, options?: FocusOptions): void;
282282
_getHostElement(): HTMLElement;
283283
getLabel(): string;
284-
_getTabIndex(): string;
285284
// (undocumented)
286285
readonly group: _MatOptgroupBase;
287286
_handleKeydown(event: KeyboardEvent): void;

tools/public_api_guard/material/legacy-core.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ export { _MatLegacyOptgroupBase }
173173
// @public @deprecated
174174
export class MatLegacyOption<T = any> extends _MatLegacyOptionBase<T> {
175175
constructor(element: ElementRef<HTMLElement>, changeDetectorRef: ChangeDetectorRef, parent: MatLegacyOptionParentComponent, group: MatLegacyOptgroup);
176+
_getTabIndex(): string;
176177
// (undocumented)
177178
static ɵcmp: i0.ɵɵComponentDeclaration<MatLegacyOption<any>, "mat-option", ["matOption"], {}, {}, never, ["*"], false, never>;
178179
// (undocumented)

0 commit comments

Comments
 (0)