Skip to content

Commit f81f67d

Browse files
authored
feat(material/tabs): allow for content tabindex to be customized (#21912)
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 c08e2c9 commit f81f67d

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
@@ -6,6 +6,7 @@
66
@include layout-common.fill;
77
display: block;
88
overflow: hidden;
9+
outline: 0;
910

1011
// Fix for auto content wrapping in IE11
1112
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
@@ -349,6 +349,20 @@ describe('MDC-based MatTabGroup', () => {
349349
expect(tabHeader.focusIndex).not.toBe(3);
350350
});
351351

352+
it('should be able to set a tabindex on the inner content element', () => {
353+
fixture.componentInstance.contentTabIndex = 1;
354+
fixture.detectChanges();
355+
const contentElements = Array.from<HTMLElement>(fixture.nativeElement
356+
.querySelectorAll('mat-tab-body'));
357+
358+
expect(contentElements.map(e => e.getAttribute('tabindex'))).toEqual([null, '1', null]);
359+
360+
fixture.componentInstance.selectedIndex = 0;
361+
fixture.detectChanges();
362+
363+
expect(contentElements.map(e => e.getAttribute('tabindex'))).toEqual(['1', null, null]);
364+
});
365+
352366
});
353367

354368
describe('aria labelling', () => {
@@ -830,6 +844,7 @@ describe('MatTabNavBar with a default config', () => {
830844
[(selectedIndex)]="selectedIndex"
831845
[headerPosition]="headerPosition"
832846
[disableRipple]="disableRipple"
847+
[contentTabIndex]="contentTabIndex"
833848
(animationDone)="animationDone()"
834849
(focusChange)="handleFocus($event)"
835850
(selectedTabChange)="handleSelection($event)">
@@ -855,6 +870,7 @@ class SimpleTabsTestApp {
855870
focusEvent: any;
856871
selectEvent: any;
857872
disableRipple: boolean = false;
873+
contentTabIndex: number | null = null;
858874
headerPosition: MatTabHeaderPosition = 'above';
859875
handleFocus(event: any) {
860876
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 layout-common.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
@@ -348,6 +348,20 @@ describe('MatTabGroup', () => {
348348
expect(tabHeader.focusIndex).not.toBe(3);
349349
});
350350

351+
it('should be able to set a tabindex on the inner content element', () => {
352+
fixture.componentInstance.contentTabIndex = 1;
353+
fixture.detectChanges();
354+
const contentElements = Array.from<HTMLElement>(fixture.nativeElement
355+
.querySelectorAll('mat-tab-body'));
356+
357+
expect(contentElements.map(e => e.getAttribute('tabindex'))).toEqual([null, '1', null]);
358+
359+
fixture.componentInstance.selectedIndex = 0;
360+
fixture.detectChanges();
361+
362+
expect(contentElements.map(e => e.getAttribute('tabindex'))).toEqual(['1', null, null]);
363+
});
364+
351365
});
352366

353367
describe('aria labelling', () => {
@@ -774,6 +788,7 @@ describe('nested MatTabGroup with enabled animations', () => {
774788
[(selectedIndex)]="selectedIndex"
775789
[headerPosition]="headerPosition"
776790
[disableRipple]="disableRipple"
791+
[contentTabIndex]="contentTabIndex"
777792
(animationDone)="animationDone()"
778793
(focusChange)="handleFocus($event)"
779794
(selectedTabChange)="handleSelection($event)">
@@ -799,6 +814,7 @@ class SimpleTabsTestApp {
799814
focusEvent: any;
800815
selectEvent: any;
801816
disableRipple: boolean = false;
817+
contentTabIndex: number | null = null;
802818
headerPosition: MatTabHeaderPosition = 'above';
803819
handleFocus(event: any) {
804820
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
/**
@@ -397,6 +411,7 @@ export abstract class _MatTabGroupBase extends _MatTabGroupMixinBase implements
397411
static ngAcceptInputType_animationDuration: NumberInput;
398412
static ngAcceptInputType_selectedIndex: NumberInput;
399413
static ngAcceptInputType_disableRipple: BooleanInput;
414+
static ngAcceptInputType_contentTabIndex: BooleanInput;
400415
}
401416

402417
/**

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);
@@ -65,10 +67,11 @@ export declare abstract class _MatTabGroupBase extends _MatTabGroupMixinBase imp
6567
ngOnDestroy(): void;
6668
realignInkBar(): void;
6769
static ngAcceptInputType_animationDuration: NumberInput;
70+
static ngAcceptInputType_contentTabIndex: BooleanInput;
6871
static ngAcceptInputType_disableRipple: BooleanInput;
6972
static ngAcceptInputType_dynamicHeight: BooleanInput;
7073
static ngAcceptInputType_selectedIndex: NumberInput;
71-
static ɵdir: i0.ɵɵDirectiveDeclaration<_MatTabGroupBase, never, never, { "dynamicHeight": "dynamicHeight"; "selectedIndex": "selectedIndex"; "headerPosition": "headerPosition"; "animationDuration": "animationDuration"; "disablePagination": "disablePagination"; "backgroundColor": "backgroundColor"; }, { "selectedIndexChange": "selectedIndexChange"; "focusChange": "focusChange"; "animationDone": "animationDone"; "selectedTabChange": "selectedTabChange"; }, never>;
74+
static ɵdir: i0.ɵɵDirectiveDeclaration<_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>;
7275
static ɵfac: i0.ɵɵFactoryDeclaration<_MatTabGroupBase, [null, null, { optional: true; }, { optional: true; }]>;
7376
}
7477

@@ -251,6 +254,7 @@ export declare const matTabsAnimations: {
251254

252255
export interface MatTabsConfig {
253256
animationDuration?: string;
257+
contentTabIndex?: number;
254258
disablePagination?: boolean;
255259
dynamicHeight?: boolean;
256260
fitInkBarToContent?: boolean;

0 commit comments

Comments
 (0)