Skip to content

Commit 64792d6

Browse files
committed
fix(material/expansion): unable to assign custom tabindex on header
Adds the ability to assign a custom `tabindex` to the expansion panel header. Fixes #22521.
1 parent f2b94e8 commit 64792d6

File tree

3 files changed

+54
-7
lines changed

3 files changed

+54
-7
lines changed

src/material/expansion/expansion-panel-header.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {FocusableOption, FocusMonitor, FocusOrigin} from '@angular/cdk/a11y';
1010
import {ENTER, hasModifierKey, SPACE} from '@angular/cdk/keycodes';
1111
import {
1212
AfterViewInit,
13+
Attribute,
1314
ChangeDetectionStrategy,
1415
ChangeDetectorRef,
1516
Component,
@@ -23,6 +24,8 @@ import {
2324
ViewEncapsulation,
2425
} from '@angular/core';
2526
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
27+
import {HasTabIndex, HasTabIndexCtor, mixinTabIndex} from '@angular/material/core';
28+
import {NumberInput} from '@angular/cdk/coercion';
2629
import {EMPTY, merge, Subscription} from 'rxjs';
2730
import {filter} from 'rxjs/operators';
2831
import {MatAccordionTogglePosition} from './accordion-base';
@@ -34,6 +37,15 @@ import {
3437
} from './expansion-panel';
3538

3639

40+
// Boilerplate for applying mixins to MatExpansionPanelHeader.
41+
/** @docs-private */
42+
abstract class MatExpansionPanelHeaderBase {
43+
abstract readonly disabled: boolean;
44+
}
45+
const _MatExpansionPanelHeaderMixinBase:
46+
HasTabIndexCtor &
47+
typeof MatExpansionPanelHeaderBase = mixinTabIndex(MatExpansionPanelHeaderBase);
48+
3749
/**
3850
* Header element of a `<mat-expansion-panel>`.
3951
*/
@@ -43,14 +55,15 @@ import {
4355
templateUrl: 'expansion-panel-header.html',
4456
encapsulation: ViewEncapsulation.None,
4557
changeDetection: ChangeDetectionStrategy.OnPush,
58+
inputs: ['tabIndex'],
4659
animations: [
4760
matExpansionAnimations.indicatorRotate,
4861
],
4962
host: {
5063
'class': 'mat-expansion-panel-header mat-focus-indicator',
5164
'role': 'button',
5265
'[attr.id]': 'panel._headerId',
53-
'[attr.tabindex]': 'disabled ? -1 : 0',
66+
'[attr.tabindex]': 'tabIndex',
5467
'[attr.aria-controls]': '_getPanelId()',
5568
'[attr.aria-expanded]': '_isExpanded()',
5669
'[attr.aria-disabled]': 'panel.disabled',
@@ -63,7 +76,8 @@ import {
6376
'(keydown)': '_keydown($event)',
6477
},
6578
})
66-
export class MatExpansionPanelHeader implements AfterViewInit, OnDestroy, FocusableOption {
79+
export class MatExpansionPanelHeader extends _MatExpansionPanelHeaderMixinBase implements
80+
AfterViewInit, OnDestroy, FocusableOption, HasTabIndex {
6781
private _parentChangeSubscription = Subscription.EMPTY;
6882

6983
constructor(
@@ -73,11 +87,14 @@ export class MatExpansionPanelHeader implements AfterViewInit, OnDestroy, Focusa
7387
private _changeDetectorRef: ChangeDetectorRef,
7488
@Inject(MAT_EXPANSION_PANEL_DEFAULT_OPTIONS) @Optional()
7589
defaultOptions?: MatExpansionPanelDefaultOptions,
76-
@Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string) {
90+
@Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string,
91+
@Attribute('tabindex') tabIndex?: string) {
92+
super();
7793
const accordionHideToggleChange = panel.accordion ?
7894
panel.accordion._stateChanges.pipe(
7995
filter(changes => !!(changes['hideToggle'] || changes['togglePosition']))) :
8096
EMPTY;
97+
this.tabIndex = parseInt(tabIndex || '') || 0;
8198

8299
// Since the toggle state depends on an @Input on the panel, we
83100
// need to subscribe and trigger change detection manually.
@@ -210,6 +227,8 @@ export class MatExpansionPanelHeader implements AfterViewInit, OnDestroy, Focusa
210227
this._parentChangeSubscription.unsubscribe();
211228
this._focusMonitor.stopMonitoring(this._element);
212229
}
230+
231+
static ngAcceptInputType_tabIndex: NumberInput;
213232
}
214233

215234
/**

src/material/expansion/expansion.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ describe('MatExpansionPanel', () => {
3737
LazyPanelWithContent,
3838
LazyPanelOpenOnLoad,
3939
PanelWithTwoWayBinding,
40+
PanelWithHeaderTabindex,
4041
],
4142
});
4243
TestBed.compileComponents();
@@ -383,6 +384,14 @@ describe('MatExpansionPanel', () => {
383384
expect(header.nativeElement.style.height).toBe('10px');
384385
});
385386

387+
it('should be able to set a custom tabindex on the header', fakeAsync(() => {
388+
const fixture = TestBed.createComponent(PanelWithHeaderTabindex);
389+
const headerEl = fixture.nativeElement.querySelector('.mat-expansion-panel-header');
390+
fixture.detectChanges();
391+
392+
expect(headerEl.getAttribute('tabindex')).toBe('7');
393+
}));
394+
386395
describe('disabled state', () => {
387396
let fixture: ComponentFixture<PanelWithContent>;
388397
let panel: HTMLElement;
@@ -487,6 +496,15 @@ describe('MatExpansionPanel', () => {
487496
expect(header.classList).not.toContain('mat-expanded');
488497
});
489498

499+
it('should update the tabindex if the header becomes disabled', () => {
500+
expect(header.getAttribute('tabindex')).toBe('0');
501+
502+
fixture.componentInstance.disabled = true;
503+
fixture.detectChanges();
504+
505+
expect(header.getAttribute('tabindex')).toBe('-1');
506+
});
507+
490508
});
491509
});
492510

@@ -578,3 +596,12 @@ class LazyPanelOpenOnLoad {}
578596
class PanelWithTwoWayBinding {
579597
expanded = false;
580598
}
599+
600+
@Component({
601+
template: `
602+
<mat-expansion-panel>
603+
<mat-expansion-panel-header tabindex="7">Panel Title</mat-expansion-panel-header>
604+
</mat-expansion-panel>`
605+
})
606+
class PanelWithHeaderTabindex {
607+
}

tools/public_api_guard/material/expansion.d.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,13 @@ export declare class MatExpansionPanelDescription {
9595
static ɵfac: i0.ɵɵFactoryDeclaration<MatExpansionPanelDescription, never>;
9696
}
9797

98-
export declare class MatExpansionPanelHeader implements AfterViewInit, OnDestroy, FocusableOption {
98+
export declare class MatExpansionPanelHeader extends _MatExpansionPanelHeaderMixinBase implements AfterViewInit, OnDestroy, FocusableOption, HasTabIndex {
9999
_animationMode?: string | undefined;
100100
collapsedHeight: string;
101101
get disabled(): boolean;
102102
expandedHeight: string;
103103
panel: MatExpansionPanel;
104-
constructor(panel: MatExpansionPanel, _element: ElementRef, _focusMonitor: FocusMonitor, _changeDetectorRef: ChangeDetectorRef, defaultOptions?: MatExpansionPanelDefaultOptions, _animationMode?: string | undefined);
104+
constructor(panel: MatExpansionPanel, _element: ElementRef, _focusMonitor: FocusMonitor, _changeDetectorRef: ChangeDetectorRef, defaultOptions?: MatExpansionPanelDefaultOptions, _animationMode?: string | undefined, tabIndex?: string);
105105
_getExpandedState(): string;
106106
_getHeaderHeight(): string | null;
107107
_getPanelId(): string;
@@ -113,8 +113,9 @@ export declare class MatExpansionPanelHeader implements AfterViewInit, OnDestroy
113113
focus(origin?: FocusOrigin, options?: FocusOptions): void;
114114
ngAfterViewInit(): void;
115115
ngOnDestroy(): void;
116-
static ɵcmp: i0.ɵɵComponentDeclaration<MatExpansionPanelHeader, "mat-expansion-panel-header", never, { "expandedHeight": "expandedHeight"; "collapsedHeight": "collapsedHeight"; }, {}, never, ["mat-panel-title", "mat-panel-description", "*"]>;
117-
static ɵfac: i0.ɵɵFactoryDeclaration<MatExpansionPanelHeader, [{ host: true; }, null, null, null, { optional: true; }, { optional: true; }]>;
116+
static ngAcceptInputType_tabIndex: NumberInput;
117+
static ɵcmp: i0.ɵɵComponentDeclaration<MatExpansionPanelHeader, "mat-expansion-panel-header", never, { "tabIndex": "tabIndex"; "expandedHeight": "expandedHeight"; "collapsedHeight": "collapsedHeight"; }, {}, never, ["mat-panel-title", "mat-panel-description", "*"]>;
118+
static ɵfac: i0.ɵɵFactoryDeclaration<MatExpansionPanelHeader, [{ host: true; }, null, null, null, { optional: true; }, { optional: true; }, { attribute: "tabindex"; }]>;
118119
}
119120

120121
export declare type MatExpansionPanelState = 'expanded' | 'collapsed';

0 commit comments

Comments
 (0)