Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions goldens/aria/combobox/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ export class ComboboxDialog {
// (undocumented)
close(): void;
readonly combobox: Combobox<any>;
readonly element: HTMLElement;
readonly element: HTMLDialogElement;
readonly id: _angular_core.InputSignal<string>;
// (undocumented)
readonly _pattern: ComboboxDialogPattern;
// (undocumented)
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<ComboboxDialog, "dialog[ngComboboxDialog]", ["ngComboboxDialog"], {}, {}, never, never, true, [{ directive: typeof ComboboxPopup; inputs: {}; outputs: {}; }]>;
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<ComboboxDialog, "dialog[ngComboboxDialog]", ["ngComboboxDialog"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; }, {}, never, never, true, [{ directive: typeof ComboboxPopup; inputs: {}; outputs: {}; }]>;
// (undocumented)
static ɵfac: _angular_core.ɵɵFactoryDeclaration<ComboboxDialog, never>;
}
Expand Down
6 changes: 3 additions & 3 deletions src/aria/accordion/accordion-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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());
});
}
Expand Down
1 change: 1 addition & 0 deletions src/aria/combobox/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ ng_project(
deps = [
"//:node_modules/@angular/core",
"//src/aria/private",
"//src/cdk/a11y",
"//src/cdk/bidi",
],
)
Expand Down
30 changes: 15 additions & 15 deletions src/aria/combobox/combobox-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -46,35 +47,34 @@ export class ComboboxDialog {
private readonly _elementRef = inject(ElementRef<HTMLDialogElement>);

/** 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<unknown>>(ComboboxPopup, {
optional: true,
});

readonly _pattern: ComboboxDialogPattern;
readonly _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();
},
});
}

Expand Down
10 changes: 6 additions & 4 deletions src/aria/combobox/combobox-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,12 @@ export class ComboboxInput {
}

/** Focuses & selects the first item in the combobox if the user changes the input value. */
afterRenderEffect(() => {
this.value();
controls?.items();
untracked(() => this.combobox._pattern.onFilter());
afterRenderEffect({
write: () => {
this.value();
controls?.items();
untracked(() => this.combobox._pattern.onFilter());
},
});
}
}
10 changes: 5 additions & 5 deletions src/aria/combobox/combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
*/

import {
afterRenderEffect,
Directive,
ElementRef,
booleanAttribute,
computed,
contentChild,
Directive,
ElementRef,
effect,
inject,
input,
signal,
Expand Down Expand Up @@ -134,13 +134,13 @@ export class Combobox<V> {
});

constructor() {
afterRenderEffect(() => {
effect(() => {
if (this.alwaysExpanded()) {
this._pattern.expanded.set(true);
}
});

afterRenderEffect(() => {
effect(() => {
if (
!this._deferredContentAware?.contentVisible() &&
(this._pattern.isFocused() || this.alwaysExpanded())
Expand Down
24 changes: 14 additions & 10 deletions src/aria/grid/grid-cell-widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
},
});
}

Expand Down
27 changes: 22 additions & 5 deletions src/aria/grid/grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,28 @@ export class Grid {
});

constructor() {
afterRenderEffect(() => this._pattern.setDefaultStateEffect());
afterRenderEffect(() => this._pattern.resetStateEffect());
afterRenderEffect(() => this._pattern.resetFocusEffect());
afterRenderEffect(() => this._pattern.restoreFocusEffect());
afterRenderEffect(() => this._pattern.focusEffect());
const ngZone = inject(NgZone);

// Since `pointermove` fires on each pixel, we need to
// be careful not to hit the zone unless it's necessary.
ngZone.runOutsideAngular(() => {
this.element.addEventListener(
'pointermove',
event => {
if (this._pattern.acceptsPointerMove()) {
ngZone.run(() => this._pattern.onPointermove(event));
}
},
{passive: true},
);
});

// 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. */
Expand Down
35 changes: 19 additions & 16 deletions src/aria/listbox/listbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
computed,
contentChildren,
Directive,
effect,
ElementRef,
inject,
input,
Expand Down Expand Up @@ -158,7 +159,7 @@ export class Listbox<V> {
this._popup._controls.set(this._pattern as ComboboxListboxPattern<V>);
}

afterRenderEffect(() => {
effect(() => {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
const violations = this._pattern.validate();
for (const violation of violations) {
Expand All @@ -167,29 +168,31 @@ export class Listbox<V> {
}
});

afterRenderEffect(() => {
this._pattern.setDefaultStateEffect();
});
afterRenderEffect({write: () => this._pattern.setDefaultStateEffect()});

// Ensure that if the active item is removed from
// the list, the listbox updates it's focus state.
afterRenderEffect(() => {
const items = inputs.items();
const activeItem = untracked(() => inputs.activeItem());
afterRenderEffect({
write: () => {
const items = inputs.items();
const activeItem = untracked(() => inputs.activeItem());

if (!items.some(i => i === activeItem) && activeItem) {
this._pattern.listBehavior.unfocus();
}
if (!items.some(i => i === activeItem) && activeItem) {
this._pattern.listBehavior.unfocus();
}
},
});

// Ensure that the value is always in sync with the available options.
afterRenderEffect(() => {
const items = inputs.items();
const value = untracked(() => this.value());
afterRenderEffect({
write: () => {
const items = inputs.items();
const value = untracked(() => this.value());

if (items && value.some(v => !items.some(i => i.value() === v))) {
this.value.set(value.filter(v => items.some(i => i.value() === v)));
}
if (items && value.some(v => !items.some(i => i.value() === v))) {
this.value.set(value.filter(v => items.some(i => i.value() === v)));
}
},
});
}

Expand Down
10 changes: 2 additions & 8 deletions src/aria/menu/menu-bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export class MenuBar<V> {
readonly _pattern: MenuBarPattern<V>;

/** The menu items as a writable signal. */
private readonly _itemPatterns = signal<any[]>([]);
private readonly _itemPatterns = computed(() => this._items().map(i => i._pattern));

/** A callback function triggered when a menu item is selected. */
readonly itemSelected = output<V>();
Expand All @@ -123,13 +123,7 @@ export class MenuBar<V> {
element: computed(() => this._elementRef.nativeElement),
});

afterRenderEffect(() => {
this._itemPatterns.set(this._items().map(i => i._pattern));
});

afterRenderEffect(() => {
this._pattern.setDefaultStateEffect();
});
afterRenderEffect({write: () => this._pattern.setDefaultState()});
}

/** Closes the menubar. */
Expand Down
25 changes: 13 additions & 12 deletions src/aria/menu/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
*/

import {
Directive,
ElementRef,
afterRenderEffect,
booleanAttribute,
computed,
contentChildren,
Directive,
ElementRef,
effect,
inject,
input,
output,
Expand Down Expand Up @@ -125,7 +126,7 @@ export class Menu<V> {
* sometimes the items array is empty. The bug can be reproduced by switching this to use a
* computed and then quickly opening and closing menus in the dev app.
*/
private readonly _itemPatterns = () => this._items().map(i => i._pattern);
private readonly _itemPatterns = computed(() => this._items().map(i => i._pattern));

/** Whether the menu is visible. */
readonly visible = computed(() => this._pattern.visible());
Expand Down Expand Up @@ -154,7 +155,7 @@ export class Menu<V> {
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);
Expand All @@ -169,16 +170,16 @@ export class Menu<V> {
// 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(() => {
this._pattern.setDefaultStateEffect();
});
afterRenderEffect({write: () => this._pattern.setDefaultStateEffect()});
}

/** Closes the menu. */
Expand Down
20 changes: 11 additions & 9 deletions src/aria/private/deferred-content/deferred-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
},
});
}

Expand Down
Loading
Loading