Skip to content

bug(aria/accordion): NG0950 when a trigger is destroyed before its first change detection #34060

Open

Description

@nagahara-naoki

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

  1. Check out angular/components at e59fce84b. This includes the accordion listener changes from fix(aria/accordion): move listeners to trigger #34053.
  2. Install Node.js from .nvmrc and pnpm from package.json#packageManager, then run pnpm install --frozen-lockfile.
  3. Append the following fixture and tests to src/aria/accordion/accordion.spec.ts as a separate top-level describe, outside the existing describe('AccordionGroup'). Its afterEach expects its own initialized fixture. The imports listed below are already present in that file; reuse them rather than duplicating them.
  4. 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions