From b2f6babde6f94236eab98e24ae859f2c124224b5 Mon Sep 17 00:00:00 2001 From: Andrey Dolgachev Date: Wed, 8 Apr 2026 18:25:09 -0400 Subject: [PATCH 1/7] refactor(multiple): use effect for setting contentVisible --- src/aria/accordion/accordion-panel.ts | 6 +++--- src/aria/combobox/combobox.ts | 10 +++++----- src/aria/menu/menu.ts | 7 ++++--- src/aria/tabs/tab-panel.ts | 4 ++-- src/aria/tree/tree-item.ts | 13 +++++++------ 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/aria/accordion/accordion-panel.ts b/src/aria/accordion/accordion-panel.ts index ad4a0762c90e..1ba13b134d9b 100644 --- a/src/aria/accordion/accordion-panel.ts +++ b/src/aria/accordion/accordion-panel.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Directive, afterRenderEffect, computed, inject, input} from '@angular/core'; +import {Directive, effect, computed, inject, input} from '@angular/core'; import {_IdGenerator} from '@angular/cdk/a11y'; import {DeferredContentAware, AccordionTriggerPattern} from '../private'; @@ -65,8 +65,8 @@ export class AccordionPanel { _pattern?: AccordionTriggerPattern; constructor() { - // Connect the panel's hidden state to the DeferredContentAware's visibility. - afterRenderEffect(() => { + effect(() => { + // Connect the panel's hidden state to the DeferredContentAware's visibility. this._deferredContentAware.contentVisible.set(this.visible()); }); } diff --git a/src/aria/combobox/combobox.ts b/src/aria/combobox/combobox.ts index e5aa15c5b54d..b48eb1f090bf 100644 --- a/src/aria/combobox/combobox.ts +++ b/src/aria/combobox/combobox.ts @@ -7,12 +7,12 @@ */ import { - afterRenderEffect, + Directive, + ElementRef, booleanAttribute, computed, contentChild, - Directive, - ElementRef, + effect, inject, input, signal, @@ -134,13 +134,13 @@ export class Combobox { }); constructor() { - afterRenderEffect(() => { + effect(() => { if (this.alwaysExpanded()) { this._pattern.expanded.set(true); } }); - afterRenderEffect(() => { + effect(() => { if ( !this._deferredContentAware?.contentVisible() && (this._pattern.isFocused() || this.alwaysExpanded()) diff --git a/src/aria/menu/menu.ts b/src/aria/menu/menu.ts index 0f0be93de099..52a0767cb676 100644 --- a/src/aria/menu/menu.ts +++ b/src/aria/menu/menu.ts @@ -7,12 +7,13 @@ */ import { + Directive, + ElementRef, afterRenderEffect, booleanAttribute, computed, contentChildren, - Directive, - ElementRef, + effect, inject, input, output, @@ -154,7 +155,7 @@ export class Menu { itemSelected: (value: V) => this.itemSelected.emit(value), }); - afterRenderEffect(() => { + effect(() => { const parent = this.parent(); if (parent instanceof MenuItem && parent.parent instanceof MenuBar) { this._deferredContentAware?.contentVisible.set(true); diff --git a/src/aria/tabs/tab-panel.ts b/src/aria/tabs/tab-panel.ts index 31f594e7443c..042cf5c0d3ad 100644 --- a/src/aria/tabs/tab-panel.ts +++ b/src/aria/tabs/tab-panel.ts @@ -11,9 +11,9 @@ import { computed, Directive, ElementRef, + effect, inject, input, - afterRenderEffect, OnInit, OnDestroy, } from '@angular/core'; @@ -90,7 +90,7 @@ export class TabPanel implements OnInit, OnDestroy { }); constructor() { - afterRenderEffect(() => this._deferredContentAware.contentVisible.set(this.visible())); + effect(() => this._deferredContentAware.contentVisible.set(this.visible())); } ngOnInit() { diff --git a/src/aria/tree/tree-item.ts b/src/aria/tree/tree-item.ts index 728ec42701d5..961e8cf0536a 100644 --- a/src/aria/tree/tree-item.ts +++ b/src/aria/tree/tree-item.ts @@ -9,7 +9,6 @@ import { Directive, ElementRef, - afterRenderEffect, booleanAttribute, computed, inject, @@ -20,6 +19,7 @@ import { OnInit, OnDestroy, afterNextRender, + effect, } from '@angular/core'; import {_IdGenerator} from '@angular/cdk/a11y'; import {ComboboxTreePattern, TreeItemPattern, DeferredContentAware} from '../private'; @@ -128,11 +128,12 @@ export class TreeItem extends DeferredContentAware implements OnInit, OnDestr this.preserveContent.set(true); } }); - // Connect the group's hidden state to the DeferredContentAware's visibility. - afterRenderEffect(() => { - this.tree()._pattern instanceof ComboboxTreePattern - ? this.contentVisible.set(true) - : this.contentVisible.set(this._pattern.expanded()); + + effect(() => { + // Connect the group's hidden state to the DeferredContentAware's visibility. + this.contentVisible.set( + this.tree()._pattern instanceof ComboboxTreePattern || this._pattern.expanded(), + ); }); } From 150e2d0ec9eda98b3290a4484cc7d8e6586e9cff Mon Sep 17 00:00:00 2001 From: Andrey Dolgachev Date: Wed, 8 Apr 2026 20:12:14 -0400 Subject: [PATCH 2/7] refactor(aria/combobox): clean up afterRenderEffect usage and fix missing id --- goldens/aria/combobox/index.api.md | 5 +++-- src/aria/combobox/BUILD.bazel | 1 + src/aria/combobox/combobox-dialog.ts | 30 ++++++++++++++-------------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/goldens/aria/combobox/index.api.md b/goldens/aria/combobox/index.api.md index 57dbe14c8fe1..09c117d40645 100644 --- a/goldens/aria/combobox/index.api.md +++ b/goldens/aria/combobox/index.api.md @@ -36,11 +36,12 @@ export class ComboboxDialog { // (undocumented) close(): void; readonly combobox: Combobox; - readonly element: HTMLElement; + readonly element: HTMLDialogElement; + readonly id: _angular_core.InputSignal; // (undocumented) _pattern: ComboboxDialogPattern; // (undocumented) - static ɵdir: _angular_core.ɵɵDirectiveDeclaration; + static ɵdir: _angular_core.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: _angular_core.ɵɵFactoryDeclaration; } diff --git a/src/aria/combobox/BUILD.bazel b/src/aria/combobox/BUILD.bazel index e0b7cbae2ad0..80905cae2544 100644 --- a/src/aria/combobox/BUILD.bazel +++ b/src/aria/combobox/BUILD.bazel @@ -11,6 +11,7 @@ ng_project( deps = [ "//:node_modules/@angular/core", "//src/aria/private", + "//src/cdk/a11y", "//src/cdk/bidi", ], ) diff --git a/src/aria/combobox/combobox-dialog.ts b/src/aria/combobox/combobox-dialog.ts index 903d3087d3c0..f370d552a70a 100644 --- a/src/aria/combobox/combobox-dialog.ts +++ b/src/aria/combobox/combobox-dialog.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.dev/license */ -import {afterRenderEffect, Directive, ElementRef, inject} from '@angular/core'; +import {afterRenderEffect, Directive, ElementRef, inject, input} from '@angular/core'; +import {_IdGenerator} from '@angular/cdk/a11y'; import {ComboboxDialogPattern} from '../private'; import {Combobox} from './combobox'; import {ComboboxPopup} from './combobox-popup'; @@ -46,35 +47,34 @@ export class ComboboxDialog { private readonly _elementRef = inject(ElementRef); /** A reference to the dialog element. */ - readonly element = this._elementRef.nativeElement as HTMLElement; + readonly element = this._elementRef.nativeElement as HTMLDialogElement; /** The combobox that the dialog belongs to. */ readonly combobox = inject(Combobox); + /** The unique identifier for the trigger. */ + readonly id = input(inject(_IdGenerator).getId('ng-combobox-dialog-', true)); + /** A reference to the parent combobox popup, if one exists. */ private readonly _popup = inject>(ComboboxPopup, { optional: true, }); - _pattern: ComboboxDialogPattern; + _pattern: ComboboxDialogPattern = new ComboboxDialogPattern({ + id: this.id, + element: () => this.element, + combobox: this.combobox._pattern, + }); constructor() { - this._pattern = new ComboboxDialogPattern({ - id: () => '', - element: () => this._elementRef.nativeElement, - combobox: this.combobox._pattern, - }); - if (this._popup) { this._popup._controls.set(this._pattern); } - afterRenderEffect(() => { - if (this._elementRef) { - this.combobox._pattern.expanded() - ? this._elementRef.nativeElement.showModal() - : this._elementRef.nativeElement.close(); - } + afterRenderEffect({ + write: () => { + this.combobox._pattern.expanded() ? this.element.showModal() : this.element.close(); + }, }); } From a59eca541e7852622e770c40b2088fbc101977cf Mon Sep 17 00:00:00 2001 From: Andrey Dolgachev Date: Wed, 8 Apr 2026 20:38:29 -0400 Subject: [PATCH 3/7] refactor(aria/grid): use read/write appropariately in afterRenderEffect --- src/aria/grid/grid-cell-widget.ts | 24 ++++++++++++++---------- src/aria/grid/grid.ts | 11 ++++++----- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/aria/grid/grid-cell-widget.ts b/src/aria/grid/grid-cell-widget.ts index 4cf9312e84eb..eb2ebf640477 100644 --- a/src/aria/grid/grid-cell-widget.ts +++ b/src/aria/grid/grid-cell-widget.ts @@ -108,18 +108,22 @@ export class GridCellWidget { } constructor() { - afterRenderEffect(() => { - const activateEvent = this._pattern.lastActivateEvent(); - if (activateEvent) { - this.activated.emit(activateEvent); - } + afterRenderEffect({ + read: () => { + const activateEvent = this._pattern.lastActivateEvent(); + if (activateEvent) { + this.activated.emit(activateEvent); + } + }, }); - afterRenderEffect(() => { - const deactivateEvent = this._pattern.lastDeactivateEvent(); - if (deactivateEvent) { - this.deactivated.emit(deactivateEvent); - } + afterRenderEffect({ + read: () => { + const deactivateEvent = this._pattern.lastDeactivateEvent(); + if (deactivateEvent) { + this.deactivated.emit(deactivateEvent); + } + }, }); } diff --git a/src/aria/grid/grid.ts b/src/aria/grid/grid.ts index 7799f2defb40..3f20ecbcdfcb 100644 --- a/src/aria/grid/grid.ts +++ b/src/aria/grid/grid.ts @@ -150,11 +150,12 @@ export class Grid { ); }); - afterRenderEffect(() => this._pattern.setDefaultStateEffect()); - afterRenderEffect(() => this._pattern.resetStateEffect()); - afterRenderEffect(() => this._pattern.resetFocusEffect()); - afterRenderEffect(() => this._pattern.restoreFocusEffect()); - afterRenderEffect(() => this._pattern.focusEffect()); + // Use Write mode for all direct DOM focus management actions. + afterRenderEffect({write: () => this._pattern.setDefaultStateEffect()}); + afterRenderEffect({write: () => this._pattern.resetStateEffect()}); + afterRenderEffect({write: () => this._pattern.resetFocusEffect()}); + afterRenderEffect({write: () => this._pattern.restoreFocusEffect()}); + afterRenderEffect({write: () => this._pattern.focusEffect()}); } /** Gets the cell pattern for a given element. */ From 8219032a6d5ebbc4ebfec7162b63a908a1215431 Mon Sep 17 00:00:00 2001 From: Andrey Dolgachev Date: Wed, 8 Apr 2026 17:36:44 -0400 Subject: [PATCH 4/7] refactor(aria/menu): move item patterns into computed signal instead of afterRenderEffect --- src/aria/menu/menu-bar.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/aria/menu/menu-bar.ts b/src/aria/menu/menu-bar.ts index ddc1b6f5601c..7dc5204b7d75 100644 --- a/src/aria/menu/menu-bar.ts +++ b/src/aria/menu/menu-bar.ts @@ -104,7 +104,7 @@ export class MenuBar { readonly _pattern: MenuBarPattern; /** The menu items as a writable signal. */ - private readonly _itemPatterns = signal([]); + private readonly _itemPatterns = computed(() => this._items().map(i => i._pattern)); /** A callback function triggered when a menu item is selected. */ readonly itemSelected = output(); @@ -123,10 +123,6 @@ export class MenuBar { element: computed(() => this._elementRef.nativeElement), }); - afterRenderEffect(() => { - this._itemPatterns.set(this._items().map(i => i._pattern)); - }); - afterRenderEffect(() => { this._pattern.setDefaultStateEffect(); }); From 4a8a766927e4fed279769d466688b579759e5767 Mon Sep 17 00:00:00 2001 From: Andrey Dolgachev Date: Wed, 8 Apr 2026 20:23:12 -0400 Subject: [PATCH 5/7] refactor(aria/menu): use write in afterRenderEffect when setting focus --- src/aria/menu/menu.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/aria/menu/menu.ts b/src/aria/menu/menu.ts index 52a0767cb676..3d602b4c6e83 100644 --- a/src/aria/menu/menu.ts +++ b/src/aria/menu/menu.ts @@ -170,11 +170,13 @@ export class Menu { // submenus. In those cases, the ui pattern is calling focus() before the ui has a chance to // update the display property. The result is focus() being called on an element that is not // focusable. This simply retries focusing the element after render. - afterRenderEffect(() => { - if (this._pattern.visible()) { - const activeItem = untracked(() => this._pattern.inputs.activeItem()); - this._pattern.listBehavior.goto(activeItem!); - } + afterRenderEffect({ + write: () => { + if (this.visible()) { + const activeItem = untracked(() => this._pattern.inputs.activeItem()); + this._pattern.listBehavior.goto(activeItem!); + } + }, }); afterRenderEffect(() => { From d6929f1b462ff57b5e67bdec46baac43899c1b43 Mon Sep 17 00:00:00 2001 From: Andrey Dolgachev Date: Wed, 8 Apr 2026 20:26:46 -0400 Subject: [PATCH 6/7] refactor(multiple): use write in afterRenderEffect for deferred content updates --- .../deferred-content/deferred-content.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/aria/private/deferred-content/deferred-content.ts b/src/aria/private/deferred-content/deferred-content.ts index d6ee311fa768..180a51dab242 100644 --- a/src/aria/private/deferred-content/deferred-content.ts +++ b/src/aria/private/deferred-content/deferred-content.ts @@ -53,17 +53,19 @@ export class DeferredContent implements OnDestroy { readonly deferredContentAware = signal(this._deferredContentAware); constructor() { - afterRenderEffect(() => { - if (this.deferredContentAware()?.contentVisible()) { - if (!this._isRendered) { + afterRenderEffect({ + write: () => { + if (this.deferredContentAware()?.contentVisible()) { + if (!this._isRendered) { + this._destroyContent(); + this._currentViewRef = this._viewContainerRef.createEmbeddedView(this._templateRef); + this._isRendered = true; + } + } else if (!this.deferredContentAware()?.preserveContent()) { this._destroyContent(); - this._currentViewRef = this._viewContainerRef.createEmbeddedView(this._templateRef); - this._isRendered = true; + this._isRendered = false; } - } else if (!this.deferredContentAware()?.preserveContent()) { - this._destroyContent(); - this._isRendered = false; - } + }, }); } From 997e387909ed4b3d8db3ae76d32a98d02b511ad9 Mon Sep 17 00:00:00 2001 From: Andrey Dolgachev Date: Thu, 9 Apr 2026 14:00:40 -0400 Subject: [PATCH 7/7] refactor(multiple): use afterNextRender for setDefaultState across all aria directives --- src/aria/grid/grid.ts | 3 ++- src/aria/listbox/listbox.ts | 7 +++---- src/aria/menu/menu-bar.ts | 6 ++---- src/aria/tabs/tab-list.ts | 7 +++---- src/aria/toolbar/toolbar.ts | 6 ++---- src/aria/tree/tree.ts | 8 ++++++-- 6 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/aria/grid/grid.ts b/src/aria/grid/grid.ts index 3f20ecbcdfcb..21c153712e8f 100644 --- a/src/aria/grid/grid.ts +++ b/src/aria/grid/grid.ts @@ -7,6 +7,7 @@ */ import { + afterNextRender, afterRenderEffect, booleanAttribute, computed, @@ -151,7 +152,7 @@ export class Grid { }); // Use Write mode for all direct DOM focus management actions. - afterRenderEffect({write: () => this._pattern.setDefaultStateEffect()}); + afterNextRender({write: () => this._pattern.setDefaultStateEffect()}); afterRenderEffect({write: () => this._pattern.resetStateEffect()}); afterRenderEffect({write: () => this._pattern.resetFocusEffect()}); afterRenderEffect({write: () => this._pattern.restoreFocusEffect()}); diff --git a/src/aria/listbox/listbox.ts b/src/aria/listbox/listbox.ts index aba47bbb95e5..634bbc69118a 100644 --- a/src/aria/listbox/listbox.ts +++ b/src/aria/listbox/listbox.ts @@ -7,6 +7,7 @@ */ import { + afterNextRender, afterRenderEffect, booleanAttribute, computed, @@ -158,6 +159,8 @@ export class Listbox { this._popup._controls.set(this._pattern as ComboboxListboxPattern); } + afterNextRender({write: () => this._pattern.setDefaultState()}); + afterRenderEffect(() => { if (typeof ngDevMode === 'undefined' || ngDevMode) { const violations = this._pattern.validate(); @@ -167,10 +170,6 @@ export class Listbox { } }); - afterRenderEffect(() => { - this._pattern.setDefaultStateEffect(); - }); - // Ensure that if the active item is removed from // the list, the listbox updates it's focus state. afterRenderEffect(() => { diff --git a/src/aria/menu/menu-bar.ts b/src/aria/menu/menu-bar.ts index 7dc5204b7d75..6de4f36413cd 100644 --- a/src/aria/menu/menu-bar.ts +++ b/src/aria/menu/menu-bar.ts @@ -7,7 +7,7 @@ */ import { - afterRenderEffect, + afterNextRender, booleanAttribute, computed, contentChildren, @@ -123,9 +123,7 @@ export class MenuBar { element: computed(() => this._elementRef.nativeElement), }); - afterRenderEffect(() => { - this._pattern.setDefaultStateEffect(); - }); + afterNextRender({write: () => this._pattern.setDefaultState()}); } /** Closes the menubar. */ diff --git a/src/aria/tabs/tab-list.ts b/src/aria/tabs/tab-list.ts index 9932a6207aec..f354921c6a3c 100644 --- a/src/aria/tabs/tab-list.ts +++ b/src/aria/tabs/tab-list.ts @@ -8,6 +8,8 @@ import {Directionality} from '@angular/cdk/bidi'; import { + afterNextRender, + afterRenderEffect, booleanAttribute, computed, Directive, @@ -16,7 +18,6 @@ import { input, model, signal, - afterRenderEffect, OnInit, OnDestroy, } from '@angular/core'; @@ -118,9 +119,7 @@ export class TabList implements OnInit, OnDestroy { }); constructor() { - afterRenderEffect(() => { - this._pattern.setDefaultStateEffect(); - }); + afterNextRender({write: () => this._pattern.setDefaultState()}); afterRenderEffect(() => { const tab = this._pattern.selectedTab(); diff --git a/src/aria/toolbar/toolbar.ts b/src/aria/toolbar/toolbar.ts index bbb877b58129..8e5b2ee32d59 100644 --- a/src/aria/toolbar/toolbar.ts +++ b/src/aria/toolbar/toolbar.ts @@ -7,7 +7,7 @@ */ import { - afterRenderEffect, + afterNextRender, Directive, ElementRef, inject, @@ -106,9 +106,7 @@ export class Toolbar { }); constructor() { - afterRenderEffect(() => { - this._pattern.setDefaultStateEffect(); - }); + afterNextRender({write: () => this._pattern.setDefaultState()}); } _register(widget: ToolbarWidget) { diff --git a/src/aria/tree/tree.ts b/src/aria/tree/tree.ts index c50465478b8d..48a65a8760f5 100644 --- a/src/aria/tree/tree.ts +++ b/src/aria/tree/tree.ts @@ -9,6 +9,7 @@ import { Directive, ElementRef, + afterNextRender, afterRenderEffect, booleanAttribute, computed, @@ -180,8 +181,11 @@ export class Tree { } }); - afterRenderEffect(() => { - this._pattern.setDefaultStateEffect(); + // Resets default focus based on selection state until interacted. + afterRenderEffect({ + write: () => { + this._pattern.setDefaultStateEffect(); + }, }); afterRenderEffect(() => {