Skip to content

Commit a679969

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 a679969

File tree

3 files changed

+50
-7
lines changed

3 files changed

+50
-7
lines changed

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

Lines changed: 19 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,7 @@ 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';
2628
import {EMPTY, merge, Subscription} from 'rxjs';
2729
import {filter} from 'rxjs/operators';
2830
import {MatAccordionTogglePosition} from './accordion-base';
@@ -34,6 +36,15 @@ import {
3436
} from './expansion-panel';
3537

3638

39+
// Boilerplate for applying mixins to MatExpansionPanelHeader.
40+
/** @docs-private */
41+
abstract class MatExpansionPanelHeaderBase {
42+
abstract readonly disabled: boolean;
43+
}
44+
const _MatExpansionPanelHeaderMixinBase:
45+
HasTabIndexCtor &
46+
typeof MatExpansionPanelHeaderBase = mixinTabIndex(MatExpansionPanelHeaderBase);
47+
3748
/**
3849
* Header element of a `<mat-expansion-panel>`.
3950
*/
@@ -43,14 +54,15 @@ import {
4354
templateUrl: 'expansion-panel-header.html',
4455
encapsulation: ViewEncapsulation.None,
4556
changeDetection: ChangeDetectionStrategy.OnPush,
57+
inputs: ['tabIndex'],
4658
animations: [
4759
matExpansionAnimations.indicatorRotate,
4860
],
4961
host: {
5062
'class': 'mat-expansion-panel-header mat-focus-indicator',
5163
'role': 'button',
5264
'[attr.id]': 'panel._headerId',
53-
'[attr.tabindex]': 'disabled ? -1 : 0',
65+
'[attr.tabindex]': 'tabIndex',
5466
'[attr.aria-controls]': '_getPanelId()',
5567
'[attr.aria-expanded]': '_isExpanded()',
5668
'[attr.aria-disabled]': 'panel.disabled',
@@ -63,7 +75,8 @@ import {
6375
'(keydown)': '_keydown($event)',
6476
},
6577
})
66-
export class MatExpansionPanelHeader implements AfterViewInit, OnDestroy, FocusableOption {
78+
export class MatExpansionPanelHeader extends _MatExpansionPanelHeaderMixinBase implements
79+
AfterViewInit, OnDestroy, FocusableOption, HasTabIndex {
6780
private _parentChangeSubscription = Subscription.EMPTY;
6881

6982
constructor(
@@ -73,11 +86,14 @@ export class MatExpansionPanelHeader implements AfterViewInit, OnDestroy, Focusa
7386
private _changeDetectorRef: ChangeDetectorRef,
7487
@Inject(MAT_EXPANSION_PANEL_DEFAULT_OPTIONS) @Optional()
7588
defaultOptions?: MatExpansionPanelDefaultOptions,
76-
@Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string) {
89+
@Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string,
90+
@Attribute('tabindex') tabIndex?: string) {
91+
super();
7792
const accordionHideToggleChange = panel.accordion ?
7893
panel.accordion._stateChanges.pipe(
7994
filter(changes => !!(changes['hideToggle'] || changes['togglePosition']))) :
8095
EMPTY;
96+
this.tabIndex = parseInt(tabIndex || '') || 0;
8197

8298
// Since the toggle state depends on an @Input on the panel, we
8399
// need to subscribe and trigger change detection manually.

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 expand and collapse the panel', 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 toggle the tabindex of the header', () => {
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: 4 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,8 @@ 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 ɵcmp: i0.ɵɵComponentDeclaration<MatExpansionPanelHeader, "mat-expansion-panel-header", never, { "tabIndex": "tabIndex"; "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; }, { attribute: "tabindex"; }]>;
118118
}
119119

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

0 commit comments

Comments
 (0)