Skip to content

feat(material/tabs): allow for content tabindex to be customized #21912

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/material-experimental/mdc-tabs/tab-body.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
@include layout-common.fill;
display: block;
overflow: hidden;
outline: 0;

// Fix for auto content wrapping in IE11
flex-basis: 100%;
Expand Down
3 changes: 2 additions & 1 deletion src/material-experimental/mdc-tabs/tab-group.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@
<mat-tab-body role="tabpanel"
*ngFor="let tab of _tabs; let i = index"
[id]="_getTabContentId(i)"
[attr.tabindex]="(contentTabIndex != null && selectedIndex === i) ? contentTabIndex : null"
[attr.aria-labelledby]="_getTabLabelId(i)"
[class.mat-mdc-tab-body-active]="selectedIndex == i"
[class.mat-mdc-tab-body-active]="selectedIndex === i"
[content]="tab.content!"
[position]="tab.position!"
[origin]="tab.origin"
Expand Down
16 changes: 16 additions & 0 deletions src/material-experimental/mdc-tabs/tab-group.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,20 @@ describe('MDC-based MatTabGroup', () => {
expect(tabHeader.focusIndex).not.toBe(3);
});

it('should be able to set a tabindex on the inner content element', () => {
fixture.componentInstance.contentTabIndex = 1;
fixture.detectChanges();
const contentElements = Array.from<HTMLElement>(fixture.nativeElement
.querySelectorAll('mat-tab-body'));

expect(contentElements.map(e => e.getAttribute('tabindex'))).toEqual([null, '1', null]);

fixture.componentInstance.selectedIndex = 0;
fixture.detectChanges();

expect(contentElements.map(e => e.getAttribute('tabindex'))).toEqual(['1', null, null]);
});

});

describe('aria labelling', () => {
Expand Down Expand Up @@ -830,6 +844,7 @@ describe('MatTabNavBar with a default config', () => {
[(selectedIndex)]="selectedIndex"
[headerPosition]="headerPosition"
[disableRipple]="disableRipple"
[contentTabIndex]="contentTabIndex"
(animationDone)="animationDone()"
(focusChange)="handleFocus($event)"
(selectedTabChange)="handleSelection($event)">
Expand All @@ -855,6 +870,7 @@ class SimpleTabsTestApp {
focusEvent: any;
selectEvent: any;
disableRipple: boolean = false;
contentTabIndex: number | null = null;
headerPosition: MatTabHeaderPosition = 'above';
handleFocus(event: any) {
this.focusEvent = event;
Expand Down
3 changes: 3 additions & 0 deletions src/material/tabs/tab-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export interface MatTabsConfig {

/** Whether the tab group should grow to the size of the active tab. */
dynamicHeight?: boolean;

/** `tabindex` to be set on the inner element that wraps the tab content. */
contentTabIndex?: number;
}

/** Injection token that can be used to provide the default options the tabs module. */
Expand Down
3 changes: 2 additions & 1 deletion src/material/tabs/tab-group.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@
<mat-tab-body role="tabpanel"
*ngFor="let tab of _tabs; let i = index"
[id]="_getTabContentId(i)"
[attr.tabindex]="(contentTabIndex != null && selectedIndex === i) ? contentTabIndex : null"
[attr.aria-labelledby]="_getTabLabelId(i)"
[class.mat-tab-body-active]="selectedIndex == i"
[class.mat-tab-body-active]="selectedIndex === i"
[content]="tab.content!"
[position]="tab.position!"
[origin]="tab.origin"
Expand Down
1 change: 1 addition & 0 deletions src/material/tabs/tab-group.scss
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
@include layout-common.fill;
display: block;
overflow: hidden;
outline: 0;

// Fix for auto content wrapping in IE11
flex-basis: 100%;
Expand Down
16 changes: 16 additions & 0 deletions src/material/tabs/tab-group.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,20 @@ describe('MatTabGroup', () => {
expect(tabHeader.focusIndex).not.toBe(3);
});

it('should be able to set a tabindex on the inner content element', () => {
fixture.componentInstance.contentTabIndex = 1;
fixture.detectChanges();
const contentElements = Array.from<HTMLElement>(fixture.nativeElement
.querySelectorAll('mat-tab-body'));

expect(contentElements.map(e => e.getAttribute('tabindex'))).toEqual([null, '1', null]);

fixture.componentInstance.selectedIndex = 0;
fixture.detectChanges();

expect(contentElements.map(e => e.getAttribute('tabindex'))).toEqual(['1', null, null]);
});

});

describe('aria labelling', () => {
Expand Down Expand Up @@ -774,6 +788,7 @@ describe('nested MatTabGroup with enabled animations', () => {
[(selectedIndex)]="selectedIndex"
[headerPosition]="headerPosition"
[disableRipple]="disableRipple"
[contentTabIndex]="contentTabIndex"
(animationDone)="animationDone()"
(focusChange)="handleFocus($event)"
(selectedTabChange)="handleSelection($event)">
Expand All @@ -799,6 +814,7 @@ class SimpleTabsTestApp {
focusEvent: any;
selectEvent: any;
disableRipple: boolean = false;
contentTabIndex: number | null = null;
headerPosition: MatTabHeaderPosition = 'above';
handleFocus(event: any) {
this.focusEvent = event;
Expand Down
15 changes: 15 additions & 0 deletions src/material/tabs/tab-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,19 @@ export abstract class _MatTabGroupBase extends _MatTabGroupMixinBase implements
}
private _animationDuration: string;

/**
* `tabindex` to be set on the inner element that wraps the tab content. Can be used for improved
* accessibility when the tab does not have focusable elements or if it has scrollable content.
* The `tabindex` will be removed automatically for inactive tabs.
* Read more at https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-2/tabs.html
*/
@Input()
get contentTabIndex(): number | null { return this._contentTabIndex; }
set contentTabIndex(value: number | null) {
this._contentTabIndex = coerceNumberProperty(value, null);
}
private _contentTabIndex: number | null;

/**
* Whether pagination should be disabled. This can be used to avoid unnecessary
* layout recalculations if it's known that pagination won't be required.
Expand Down Expand Up @@ -182,6 +195,7 @@ export abstract class _MatTabGroupBase extends _MatTabGroupMixinBase implements
defaultConfig.disablePagination : false;
this.dynamicHeight = defaultConfig && defaultConfig.dynamicHeight != null ?
defaultConfig.dynamicHeight : false;
this.contentTabIndex = defaultConfig?.contentTabIndex ?? null;
}

/**
Expand Down Expand Up @@ -397,6 +411,7 @@ export abstract class _MatTabGroupBase extends _MatTabGroupMixinBase implements
static ngAcceptInputType_animationDuration: NumberInput;
static ngAcceptInputType_selectedIndex: NumberInput;
static ngAcceptInputType_disableRipple: BooleanInput;
static ngAcceptInputType_contentTabIndex: BooleanInput;
}

/**
Expand Down
6 changes: 5 additions & 1 deletion tools/public_api_guard/material/tabs.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export declare abstract class _MatTabGroupBase extends _MatTabGroupMixinBase imp
set animationDuration(value: string);
get backgroundColor(): ThemePalette;
set backgroundColor(value: ThemePalette);
get contentTabIndex(): number | null;
set contentTabIndex(value: number | null);
disablePagination: boolean;
get dynamicHeight(): boolean;
set dynamicHeight(value: boolean);
Expand All @@ -65,10 +67,11 @@ export declare abstract class _MatTabGroupBase extends _MatTabGroupMixinBase imp
ngOnDestroy(): void;
realignInkBar(): void;
static ngAcceptInputType_animationDuration: NumberInput;
static ngAcceptInputType_contentTabIndex: BooleanInput;
static ngAcceptInputType_disableRipple: BooleanInput;
static ngAcceptInputType_dynamicHeight: BooleanInput;
static ngAcceptInputType_selectedIndex: NumberInput;
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>;
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>;
static ɵfac: i0.ɵɵFactoryDeclaration<_MatTabGroupBase, [null, null, { optional: true; }, { optional: true; }]>;
}

Expand Down Expand Up @@ -251,6 +254,7 @@ export declare const matTabsAnimations: {

export interface MatTabsConfig {
animationDuration?: string;
contentTabIndex?: number;
disablePagination?: boolean;
dynamicHeight?: boolean;
fitInkBarToContent?: boolean;
Expand Down