Repository navigation
Description
Is this a regression?
- Yes, this behavior used to work in the previous version
The previous version in which this bug was not present was
No response
Description
AccordionTrigger.ngOnDestroy() reads the required panel() input even if the trigger is destroyed before its first change detection and before ngOnInit has run. Destroying a component synchronously after TestBed.createComponent() therefore throws NG0950, despite [panel] being bound correctly in the template.
This is an early-teardown lifecycle issue, for example in a test that creates a component without running change detection before teardown. I reproduced it with the repository Chromium runner. I have not verified how often it occurs in production applications. An initialized accordion is not affected by this reproduction.
Reproduction
The minimal failing operation, using the fixture defined below, is:
const fixture = TestBed.createComponent(StaticAccordion);
fixture.destroy(); // NG0950: required input "panel" has not been set yet.This is a synchronous creation/teardown lifecycle reproduction rather than a visible interaction. I am including complete TestBed fixtures and an exact repository test command instead of an interactive StackBlitz.
Setup and steps
- Check out angular/components at
e59fce84b. This includes the accordion listener changes from fix(aria/accordion): move listeners to trigger #34053. - Install Node.js from
.nvmrcand pnpm frompackage.json#packageManager, then runpnpm install --frozen-lockfile. - Append the following fixture and tests to
src/aria/accordion/accordion.spec.tsas a separate top-leveldescribe, outside the existingdescribe('AccordionGroup'). ItsafterEachexpects its own initialized fixture. The imports listed below are already present in that file; reuse them rather than duplicating them. - Run
pnpm test src/aria/accordion --no-watch.
// Already imported in accordion.spec.ts:
// import {Component, ChangeDetectionStrategy} from "@angular/core";
// import {TestBed} from "@angular/core/testing";
// import {AccordionGroup} from "./accordion-group";
// import {AccordionTrigger} from "./accordion-trigger";
// import {AccordionPanel} from "./accordion-panel";
// import {AccordionContent} from "./accordion-content";
@Component({
template: `
<div ngAccordionGroup>
<button ngAccordionTrigger [panel]="panel">Trigger</button>
<div ngAccordionPanel #panel="ngAccordionPanel">
<ng-template ngAccordionContent>Content</ng-template>
</div>
</div>
`,
imports: [AccordionGroup, AccordionTrigger, AccordionPanel, AccordionContent],
changeDetection: ChangeDetectionStrategy.Eager,
})
class StaticAccordion {}
describe("Accordion destruction", () => {
it("destroys before first change detection", () => {
const fixture = TestBed.createComponent(StaticAccordion);
expect(() => fixture.destroy()).not.toThrow();
});
it("destroys after initialization (control)", async () => {
const fixture = TestBed.createComponent(StaticAccordion);
await fixture.whenStable();
expect(() => fixture.destroy()).not.toThrow();
});
});Do not call fixture.detectChanges() or await fixture.whenStable() before destroying in the first test: that applies the input binding and bypasses the reproduction. In the control test, awaiting stability allows initialization and makes destruction succeed.
Keep the trigger and panel static in the template. Putting them inside @if/@for changes when they are created. The template does bind [panel] correctly; the issue is reading that binding before the first change detection has applied it.
Expected Behavior
Destroying an accordion before initialization should complete without throwing. Since ngOnInit has not associated the trigger with a panel or registered it in the group, cleanup should not read its still-unset required input.
After initialization, destruction should continue clearing the panel association and unregistering the trigger.
Actual Behavior
The early-destruction test fails with:
Expected function not to throw, but it threw Error:
NG0950: Input "panel" is required but no value is available yet.
With the two tests in the reproduction, the Chromium accordion suite reports 44 passed, 1 failed. Only the early-destruction test fails; the initialized control passes.
Cause and related PRs
AccordionTrigger at the tested revision associates the panel in ngOnInit with this.panel()._pattern = this._pattern, but unconditionally reads this.panel() again in ngOnDestroy. The early-teardown path has not applied the required input binding yet.
The same lifecycle problem was fixed for combobox in #33974 (issue #33972). Tree has a separate report #34058 and proposed fix #34059; neither includes accordion.
The accordion-only implementation and regression tests are in #34061. It retains the panel associated during initialization and skips clearing it when no association was made; the initialized cleanup is also tested.
Environment
- Components source: angular/components main at
e59fce84b(2026-11-04) - Angular framework packages used by the repository: 22.3.0-next.3
- Browser: Chromium provided by the repository Bazel browser test setup
- Operating system: macOS, Darwin 25.5.0, arm64
- Node.js: 22.23.4 (
.nvmrc) - pnpm: 12.16.1 (
packageManager) - Command:
pnpm test src/aria/accordion --no-watch - Regression status: unknown; older releases have not been tested.
Activity