Skip to content

Commit 64c2d31

Browse files
authored
fix(material/expansion): picking up lazy content from child component (#14477)
We use a `ContentChild` to determine what lazy content to render inside an expansion panel. When the lazy content is nested further down inside another expansion panel, all ancestor panels up the tree will pick up the lowest lazy content from the lowest level. These changes add a check to ensure that the lazy content is rendered out by the closest panel. Fixes #14365.
1 parent 9ad184d commit 64c2d31

File tree

6 files changed

+89
-5
lines changed

6 files changed

+89
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {InjectionToken} from '@angular/core';
10+
import {CdkAccordionItem} from '@angular/cdk/accordion';
11+
12+
/**
13+
* Base interface for a `MatExpansionPanel`.
14+
* @docs-private
15+
*/
16+
export interface MatExpansionPanelBase extends CdkAccordionItem {
17+
/** Whether the toggle indicator should be hidden. */
18+
hideToggle: boolean;
19+
}
20+
21+
/**
22+
* Token used to provide a `MatExpansionPanel` to `MatExpansionPanelContent`.
23+
* Used to avoid circular imports between `MatExpansionPanel` and `MatExpansionPanelContent`.
24+
*/
25+
export const MAT_EXPANSION_PANEL = new InjectionToken<MatExpansionPanelBase>('MAT_EXPANSION_PANEL');

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

+6-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Directive, TemplateRef} from '@angular/core';
9+
import {Directive, TemplateRef, Inject, Optional} from '@angular/core';
10+
import {MAT_EXPANSION_PANEL, MatExpansionPanelBase} from './expansion-panel-base';
1011

1112
/**
1213
* Expansion panel content that will be rendered lazily
@@ -16,5 +17,8 @@ import {Directive, TemplateRef} from '@angular/core';
1617
selector: 'ng-template[matExpansionPanelContent]',
1718
})
1819
export class MatExpansionPanelContent {
19-
constructor(public _template: TemplateRef<any>) {}
20+
constructor(
21+
public _template: TemplateRef<any>,
22+
@Inject(MAT_EXPANSION_PANEL) @Optional() public _expansionPanel?: MatExpansionPanelBase,
23+
) {}
2024
}

src/material/expansion/expansion-panel.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {Subject} from 'rxjs';
3939
import {distinctUntilChanged, filter, startWith, take} from 'rxjs/operators';
4040
import {MatAccordionBase, MatAccordionTogglePosition, MAT_ACCORDION} from './accordion-base';
4141
import {matExpansionAnimations} from './expansion-animations';
42+
import {MAT_EXPANSION_PANEL} from './expansion-panel-base';
4243
import {MatExpansionPanelContent} from './expansion-panel-content';
4344

4445
/** MatExpansionPanel's states. */
@@ -87,6 +88,7 @@ export const MAT_EXPANSION_PANEL_DEFAULT_OPTIONS =
8788
// Provide MatAccordion as undefined to prevent nested expansion panels from registering
8889
// to the same accordion.
8990
{provide: MAT_ACCORDION, useValue: undefined},
91+
{provide: MAT_EXPANSION_PANEL, useExisting: MatExpansionPanel},
9092
],
9193
host: {
9294
'class': 'mat-expansion-panel',
@@ -215,7 +217,7 @@ export class MatExpansionPanel
215217
}
216218

217219
ngAfterContentInit() {
218-
if (this._lazyContent) {
220+
if (this._lazyContent && this._lazyContent._expansionPanel === this) {
219221
// Render the content as soon as the panel becomes open.
220222
this.opened
221223
.pipe(

src/material/expansion/expansion.spec.ts

+47
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ describe('MatExpansionPanel', () => {
3131
LazyPanelOpenOnLoad,
3232
PanelWithTwoWayBinding,
3333
PanelWithHeaderTabindex,
34+
NestedLazyPanelWithContent,
3435
],
3536
});
3637
TestBed.compileComponents();
@@ -91,6 +92,36 @@ describe('MatExpansionPanel', () => {
9192
.toContain('Some content');
9293
}));
9394

95+
it('should not render lazy content from a child panel inside the parent', fakeAsync(() => {
96+
const fixture = TestBed.createComponent(NestedLazyPanelWithContent);
97+
fixture.componentInstance.parentExpanded = true;
98+
fixture.detectChanges();
99+
100+
const parentContent: HTMLElement = fixture.nativeElement.querySelector(
101+
'.parent-panel .mat-expansion-panel-content',
102+
);
103+
const childContent: HTMLElement = fixture.nativeElement.querySelector(
104+
'.child-panel .mat-expansion-panel-content',
105+
);
106+
107+
expect(parentContent.textContent!.trim()).toBe(
108+
'Parent content',
109+
'Expected only parent content to be rendered.',
110+
);
111+
expect(childContent.textContent!.trim()).toBe(
112+
'',
113+
'Expected child content element to be empty.',
114+
);
115+
116+
fixture.componentInstance.childExpanded = true;
117+
fixture.detectChanges();
118+
119+
expect(childContent.textContent!.trim()).toBe(
120+
'Child content',
121+
'Expected child content element to be rendered.',
122+
);
123+
}));
124+
94125
it('emit correct events for change in panel expanded state', () => {
95126
const fixture = TestBed.createComponent(PanelWithContent);
96127
fixture.componentInstance.expanded = true;
@@ -621,3 +652,19 @@ class PanelWithTwoWayBinding {
621652
</mat-expansion-panel>`,
622653
})
623654
class PanelWithHeaderTabindex {}
655+
656+
@Component({
657+
template: `
658+
<mat-expansion-panel class="parent-panel" [expanded]="parentExpanded">
659+
Parent content
660+
661+
<mat-expansion-panel class="child-panel" [expanded]="childExpanded">
662+
<ng-template matExpansionPanelContent>Child content</ng-template>
663+
</mat-expansion-panel>
664+
</mat-expansion-panel>
665+
`,
666+
})
667+
class NestedLazyPanelWithContent {
668+
parentExpanded = false;
669+
childExpanded = false;
670+
}

src/material/expansion/public-api.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export * from './expansion-panel';
1313
export * from './expansion-panel-header';
1414
export * from './expansion-panel-content';
1515
export * from './expansion-animations';
16+
export {MAT_EXPANSION_PANEL} from './expansion-panel-base';

tools/public_api_guard/material/expansion.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ export const EXPANSION_PANEL_ANIMATION_TIMING = "225ms cubic-bezier(0.4,0.0,0.2,
4242
// @public
4343
export const MAT_ACCORDION: InjectionToken<MatAccordionBase>;
4444

45+
// @public
46+
export const MAT_EXPANSION_PANEL: InjectionToken<MatExpansionPanelBase>;
47+
4548
// @public
4649
export const MAT_EXPANSION_PANEL_DEFAULT_OPTIONS: InjectionToken<MatExpansionPanelDefaultOptions>;
4750

@@ -142,13 +145,15 @@ export class MatExpansionPanelActionRow {
142145

143146
// @public
144147
export class MatExpansionPanelContent {
145-
constructor(_template: TemplateRef<any>);
148+
constructor(_template: TemplateRef<any>, _expansionPanel?: MatExpansionPanelBase | undefined);
149+
// (undocumented)
150+
_expansionPanel?: MatExpansionPanelBase | undefined;
146151
// (undocumented)
147152
_template: TemplateRef<any>;
148153
// (undocumented)
149154
static ɵdir: i0.ɵɵDirectiveDeclaration<MatExpansionPanelContent, "ng-template[matExpansionPanelContent]", never, {}, {}, never>;
150155
// (undocumented)
151-
static ɵfac: i0.ɵɵFactoryDeclaration<MatExpansionPanelContent, never>;
156+
static ɵfac: i0.ɵɵFactoryDeclaration<MatExpansionPanelContent, [null, { optional: true; }]>;
152157
}
153158

154159
// @public

0 commit comments

Comments
 (0)