Skip to content

Commit 223f8cf

Browse files
committed
feat(material/tabs): allow for content tabindex to be customized
In some cases, adding a `tabindex` to a `tabpanel` can improve accessibility, however that is currently impossible with our setup, because the element isn't exposed. These changes add an input that allows consumers to customize the `tabindex` based on their use case. We tried to tackle this in an earlier PR (#14808), but it was decided not to proceed, because the `tabindex` isn't relevant for all use cases. This approach should be a bit more flexible since it allows users to opt into it. Fixes #21819.
1 parent aa7dc00 commit 223f8cf

File tree

9 files changed

+61
-3
lines changed

9 files changed

+61
-3
lines changed

src/material-experimental/mdc-tabs/tab-body.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
@include mat-fill;
88
display: block;
99
overflow: hidden;
10+
outline: 0;
1011

1112
// Fix for auto content wrapping in IE11
1213
flex-basis: 100%;

src/material-experimental/mdc-tabs/tab-group.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,9 @@
5454
<mat-tab-body role="tabpanel"
5555
*ngFor="let tab of _tabs; let i = index"
5656
[id]="_getTabContentId(i)"
57+
[attr.tabindex]="(contentTabIndex != null && selectedIndex === i) ? contentTabIndex : null"
5758
[attr.aria-labelledby]="_getTabLabelId(i)"
58-
[class.mat-mdc-tab-body-active]="selectedIndex == i"
59+
[class.mat-mdc-tab-body-active]="selectedIndex === i"
5960
[content]="tab.content!"
6061
[position]="tab.position!"
6162
[origin]="tab.origin"

src/material-experimental/mdc-tabs/tab-group.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,20 @@ describe('MDC-based MatTabGroup', () => {
327327
.toHaveBeenCalledWith(jasmine.objectContaining({index: 2}));
328328
}));
329329

330+
it('should be able to set a tabindex on the inner content element', () => {
331+
fixture.componentInstance.contentTabIndex = 1;
332+
fixture.detectChanges();
333+
const contentElements = Array.from<HTMLElement>(fixture.nativeElement
334+
.querySelectorAll('mat-tab-body'));
335+
336+
expect(contentElements.map(e => e.getAttribute('tabindex'))).toEqual([null, '1', null]);
337+
338+
fixture.componentInstance.selectedIndex = 0;
339+
fixture.detectChanges();
340+
341+
expect(contentElements.map(e => e.getAttribute('tabindex'))).toEqual(['1', null, null]);
342+
});
343+
330344
});
331345

332346
describe('aria labelling', () => {
@@ -808,6 +822,7 @@ describe('MatTabNavBar with a default config', () => {
808822
[(selectedIndex)]="selectedIndex"
809823
[headerPosition]="headerPosition"
810824
[disableRipple]="disableRipple"
825+
[contentTabIndex]="contentTabIndex"
811826
(animationDone)="animationDone()"
812827
(focusChange)="handleFocus($event)"
813828
(selectedTabChange)="handleSelection($event)">
@@ -833,6 +848,7 @@ class SimpleTabsTestApp {
833848
focusEvent: any;
834849
selectEvent: any;
835850
disableRipple: boolean = false;
851+
contentTabIndex: number | null = null;
836852
headerPosition: MatTabHeaderPosition = 'above';
837853
handleFocus(event: any) {
838854
this.focusEvent = event;

src/material/tabs/tab-config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ export interface MatTabsConfig {
2626

2727
/** Whether the tab group should grow to the size of the active tab. */
2828
dynamicHeight?: boolean;
29+
30+
/** `tabindex` to be set on the inner element that wraps the tab content. */
31+
contentTabIndex?: number;
2932
}
3033

3134
/** Injection token that can be used to provide the default options the tabs module. */

src/material/tabs/tab-group.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@
4040
<mat-tab-body role="tabpanel"
4141
*ngFor="let tab of _tabs; let i = index"
4242
[id]="_getTabContentId(i)"
43+
[attr.tabindex]="(contentTabIndex != null && selectedIndex === i) ? contentTabIndex : null"
4344
[attr.aria-labelledby]="_getTabLabelId(i)"
44-
[class.mat-tab-body-active]="selectedIndex == i"
45+
[class.mat-tab-body-active]="selectedIndex === i"
4546
[content]="tab.content!"
4647
[position]="tab.position!"
4748
[origin]="tab.origin"

src/material/tabs/tab-group.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
@include mat-fill;
5151
display: block;
5252
overflow: hidden;
53+
outline: 0;
5354

5455
// Fix for auto content wrapping in IE11
5556
flex-basis: 100%;

src/material/tabs/tab-group.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,20 @@ describe('MatTabGroup', () => {
326326
.toHaveBeenCalledWith(jasmine.objectContaining({index: 2}));
327327
}));
328328

329+
it('should be able to set a tabindex on the inner content element', () => {
330+
fixture.componentInstance.contentTabIndex = 1;
331+
fixture.detectChanges();
332+
const contentElements = Array.from<HTMLElement>(fixture.nativeElement
333+
.querySelectorAll('mat-tab-body'));
334+
335+
expect(contentElements.map(e => e.getAttribute('tabindex'))).toEqual([null, '1', null]);
336+
337+
fixture.componentInstance.selectedIndex = 0;
338+
fixture.detectChanges();
339+
340+
expect(contentElements.map(e => e.getAttribute('tabindex'))).toEqual(['1', null, null]);
341+
});
342+
329343
});
330344

331345
describe('aria labelling', () => {
@@ -752,6 +766,7 @@ describe('nested MatTabGroup with enabled animations', () => {
752766
[(selectedIndex)]="selectedIndex"
753767
[headerPosition]="headerPosition"
754768
[disableRipple]="disableRipple"
769+
[contentTabIndex]="contentTabIndex"
755770
(animationDone)="animationDone()"
756771
(focusChange)="handleFocus($event)"
757772
(selectedTabChange)="handleSelection($event)">
@@ -777,6 +792,7 @@ class SimpleTabsTestApp {
777792
focusEvent: any;
778793
selectEvent: any;
779794
disableRipple: boolean = false;
795+
contentTabIndex: number | null = null;
780796
headerPosition: MatTabHeaderPosition = 'above';
781797
handleFocus(event: any) {
782798
this.focusEvent = event;

src/material/tabs/tab-group.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,19 @@ export abstract class _MatTabGroupBase extends _MatTabGroupMixinBase implements
131131
}
132132
private _animationDuration: string;
133133

134+
/**
135+
* `tabindex` to be set on the inner element that wraps the tab content. Can be used for improved
136+
* accessibility when the tab does not have focusable elements or if it has scrollable content.
137+
* The `tabindex` will be removed automatically for inactive tabs.
138+
* Read more at https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-2/tabs.html
139+
*/
140+
@Input()
141+
get contentTabIndex(): number | null { return this._contentTabIndex; }
142+
set contentTabIndex(value: number | null) {
143+
this._contentTabIndex = coerceNumberProperty(value, null);
144+
}
145+
private _contentTabIndex: number | null;
146+
134147
/**
135148
* Whether pagination should be disabled. This can be used to avoid unnecessary
136149
* layout recalculations if it's known that pagination won't be required.
@@ -182,6 +195,7 @@ export abstract class _MatTabGroupBase extends _MatTabGroupMixinBase implements
182195
defaultConfig.disablePagination : false;
183196
this.dynamicHeight = defaultConfig && defaultConfig.dynamicHeight != null ?
184197
defaultConfig.dynamicHeight : false;
198+
this.contentTabIndex = defaultConfig?.contentTabIndex ?? null;
185199
}
186200

187201
/**
@@ -385,6 +399,7 @@ export abstract class _MatTabGroupBase extends _MatTabGroupMixinBase implements
385399
static ngAcceptInputType_animationDuration: NumberInput;
386400
static ngAcceptInputType_selectedIndex: NumberInput;
387401
static ngAcceptInputType_disableRipple: BooleanInput;
402+
static ngAcceptInputType_contentTabIndex: BooleanInput;
388403
}
389404

390405
/**

tools/public_api_guard/material/tabs.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ export declare abstract class _MatTabGroupBase extends _MatTabGroupMixinBase imp
4141
set animationDuration(value: string);
4242
get backgroundColor(): ThemePalette;
4343
set backgroundColor(value: ThemePalette);
44+
get contentTabIndex(): number | null;
45+
set contentTabIndex(value: number | null);
4446
disablePagination: boolean;
4547
get dynamicHeight(): boolean;
4648
set dynamicHeight(value: boolean);
@@ -64,10 +66,11 @@ export declare abstract class _MatTabGroupBase extends _MatTabGroupMixinBase imp
6466
ngOnDestroy(): void;
6567
realignInkBar(): void;
6668
static ngAcceptInputType_animationDuration: NumberInput;
69+
static ngAcceptInputType_contentTabIndex: BooleanInput;
6770
static ngAcceptInputType_disableRipple: BooleanInput;
6871
static ngAcceptInputType_dynamicHeight: BooleanInput;
6972
static ngAcceptInputType_selectedIndex: NumberInput;
70-
static ɵdir: i0.ɵɵDirectiveDefWithMeta<_MatTabGroupBase, never, never, { "dynamicHeight": "dynamicHeight"; "selectedIndex": "selectedIndex"; "headerPosition": "headerPosition"; "animationDuration": "animationDuration"; "disablePagination": "disablePagination"; "backgroundColor": "backgroundColor"; }, { "selectedIndexChange": "selectedIndexChange"; "focusChange": "focusChange"; "animationDone": "animationDone"; "selectedTabChange": "selectedTabChange"; }, never>;
73+
static ɵdir: i0.ɵɵDirectiveDefWithMeta<_MatTabGroupBase, never, never, { "dynamicHeight": "dynamicHeight"; "selectedIndex": "selectedIndex"; "headerPosition": "headerPosition"; "animationDuration": "animationDuration"; "contentTabIndex": "contentTabIndex"; "disablePagination": "disablePagination"; "backgroundColor": "backgroundColor"; }, { "selectedIndexChange": "selectedIndexChange"; "focusChange": "focusChange"; "animationDone": "animationDone"; "selectedTabChange": "selectedTabChange"; }, never>;
7174
static ɵfac: i0.ɵɵFactoryDef<_MatTabGroupBase, [null, null, { optional: true; }, { optional: true; }]>;
7275
}
7376

@@ -250,6 +253,7 @@ export declare const matTabsAnimations: {
250253

251254
export interface MatTabsConfig {
252255
animationDuration?: string;
256+
contentTabIndex?: number;
253257
disablePagination?: boolean;
254258
dynamicHeight?: boolean;
255259
fitInkBarToContent?: boolean;

0 commit comments

Comments
 (0)