diff --git a/src/aria/accordion/accordion.ts b/src/aria/accordion/accordion.ts index 626a0aa56848..84f7f5a9d35a 100644 --- a/src/aria/accordion/accordion.ts +++ b/src/aria/accordion/accordion.ts @@ -147,15 +147,15 @@ export class AccordionTrigger { /** A reference to the trigger element. */ private readonly _elementRef = inject(ElementRef); + /** A reference to the trigger element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; + /** The parent AccordionGroup. */ private readonly _accordionGroup = inject(AccordionGroup); /** A unique identifier for the widget. */ readonly id = input(inject(_IdGenerator).getId('ng-accordion-trigger-', true)); - /** The host native element. */ - readonly element = computed(() => this._elementRef.nativeElement); - /** A local unique identifier for the trigger, used to match with its panel's `panelId`. */ readonly panelId = input.required(); @@ -176,6 +176,7 @@ export class AccordionTrigger { ...this, accordionGroup: computed(() => this._accordionGroup._pattern), accordionPanel: this._accordionPanel, + element: () => this.element, }); /** Expands this item. */ @@ -242,6 +243,9 @@ export class AccordionGroup { /** A reference to the group element. */ private readonly _elementRef = inject(ElementRef); + /** A reference to the group element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; + /** The AccordionTriggers nested inside this group. */ private readonly _triggers = contentChildren(AccordionTrigger, {descendants: true}); @@ -251,9 +255,6 @@ export class AccordionGroup { /** The AccordionPanels nested inside this group. */ private readonly _panels = contentChildren(AccordionPanel, {descendants: true}); - /** The host native element. */ - readonly element = computed(() => this._elementRef.nativeElement); - /** The text direction (ltr or rtl). */ readonly textDirection = inject(Directionality).valueSignal; @@ -280,6 +281,7 @@ export class AccordionGroup { // TODO(ok7sai): Investigate whether an accordion should support horizontal mode. orientation: () => 'vertical', getItem: e => this._getItem(e), + element: () => this.element, }); constructor() { diff --git a/src/aria/combobox/combobox.ts b/src/aria/combobox/combobox.ts index 49ce65cabf96..a1cf79e0bde9 100644 --- a/src/aria/combobox/combobox.ts +++ b/src/aria/combobox/combobox.ts @@ -91,6 +91,9 @@ export class Combobox { /** The element that the combobox is attached to. */ private readonly _elementRef = inject(ElementRef); + /** A reference to the combobox element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; + /** The DeferredContentAware host directive. */ private readonly _deferredContentAware = inject(DeferredContentAware, {optional: true}); @@ -213,6 +216,9 @@ export class ComboboxInput { /** The element that the combobox is attached to. */ private readonly _elementRef = inject>(ElementRef); + /** A reference to the input element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; + /** The combobox that the input belongs to. */ readonly combobox = inject(Combobox); @@ -330,7 +336,10 @@ export class ComboboxPopup { }) export class ComboboxDialog { /** The dialog element. */ - readonly element = inject(ElementRef); + private readonly _elementRef = inject(ElementRef); + + /** A reference to the dialog element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; /** The combobox that the dialog belongs to. */ readonly combobox = inject(Combobox); @@ -345,7 +354,7 @@ export class ComboboxDialog { constructor() { this._pattern = new ComboboxDialogPattern({ id: () => '', - element: () => this.element.nativeElement, + element: () => this._elementRef.nativeElement, combobox: this.combobox._pattern, }); @@ -354,10 +363,10 @@ export class ComboboxDialog { } afterRenderEffect(() => { - if (this.element) { + if (this._elementRef) { this.combobox._pattern.expanded() - ? this.element.nativeElement.showModal() - : this.element.nativeElement.close(); + ? this._elementRef.nativeElement.showModal() + : this._elementRef.nativeElement.close(); } }); } diff --git a/src/aria/grid/grid.ts b/src/aria/grid/grid.ts index f365396002be..61b29eee6594 100644 --- a/src/aria/grid/grid.ts +++ b/src/aria/grid/grid.ts @@ -65,6 +65,9 @@ export class Grid { /** A reference to the host element. */ private readonly _elementRef = inject(ElementRef); + /** A reference to the host element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; + /** The rows that make up the grid. */ private readonly _rows = contentChildren(GridRow, {descendants: true}); @@ -76,9 +79,6 @@ export class Grid { /** Text direction. */ readonly textDirection = inject(Directionality).valueSignal; - /** The host native element. */ - readonly element = computed(() => this._elementRef.nativeElement); - /** Whether selection is enabled for the grid. */ readonly enableSelection = input(false, {transform: booleanAttribute}); @@ -132,6 +132,7 @@ export class Grid { ...this, rows: this._rowPatterns, getCell: e => this._getCell(e), + element: () => this.element, }); constructor() { @@ -186,6 +187,9 @@ export class GridRow { /** A reference to the host element. */ private readonly _elementRef = inject(ElementRef); + /** A reference to the host element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; + /** The cells that make up this row. */ private readonly _cells = contentChildren(GridCell, {descendants: true}); @@ -200,9 +204,6 @@ export class GridRow { /** The parent grid UI pattern. */ readonly grid = computed(() => this._grid._pattern); - /** The host native element. */ - readonly element = computed(() => this._elementRef.nativeElement); - /** The index of this row within the grid. */ readonly rowIndex = input(); @@ -250,6 +251,9 @@ export class GridCell { /** A reference to the host element. */ private readonly _elementRef = inject(ElementRef); + /** A reference to the host element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; + /** The widgets contained within this cell, if any. */ private readonly _widgets = contentChildren(GridCellWidget, {descendants: true}); @@ -267,9 +271,6 @@ export class GridCell { /** A unique identifier for the cell. */ readonly id = input(inject(_IdGenerator).getId('ng-grid-cell-', true)); - /** The host native element. */ - readonly element = computed(() => this._elementRef.nativeElement); - /** The ARIA role for the cell. */ readonly role = input<'gridcell' | 'columnheader' | 'rowheader'>('gridcell'); @@ -318,6 +319,7 @@ export class GridCell { row: () => this._row._pattern, widgets: this._widgetPatterns, getWidget: e => this._getWidget(e), + element: () => this.element, }); constructor() {} @@ -369,12 +371,12 @@ export class GridCellWidget { /** A reference to the host element. */ private readonly _elementRef = inject(ElementRef); + /** A reference to the host element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; + /** The parent cell. */ private readonly _cell = inject(GridCell); - /** The host native element. */ - readonly element = computed(() => this._elementRef.nativeElement); - /** A unique identifier for the widget. */ readonly id = input(inject(_IdGenerator).getId('ng-grid-cell-widget-', true)); @@ -407,6 +409,7 @@ export class GridCellWidget { /** The UI pattern for the grid cell widget. */ readonly _pattern = new GridCellWidgetPattern({ ...this, + element: () => this.element, cell: () => this._cell._pattern, focusTarget: computed(() => { if (this.focusTarget() instanceof ElementRef) { diff --git a/src/aria/listbox/listbox.ts b/src/aria/listbox/listbox.ts index b3f3b42ee697..e9a5343c6c38 100644 --- a/src/aria/listbox/listbox.ts +++ b/src/aria/listbox/listbox.ts @@ -71,9 +71,12 @@ export class Listbox { optional: true, }); - /** A reference to the listbox element. */ + /** A reference to the host element. */ private readonly _elementRef = inject(ElementRef); + /** A reference to the host element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; + /** The directionality (LTR / RTL) context for the application (or a subtree of it). */ private readonly _directionality = inject(Directionality); @@ -233,9 +236,12 @@ export class Listbox { }, }) export class Option { - /** A reference to the option element. */ + /** A reference to the host element. */ private readonly _elementRef = inject(ElementRef); + /** A reference to the host element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; + /** The parent Listbox. */ private readonly _listbox = inject(Listbox); @@ -245,14 +251,11 @@ export class Option { // TODO(wagnermaciel): See if we want to change how we handle this since textContent is not // reactive. See https://github.com/angular/components/pull/30495#discussion_r1961260216. /** The text used by the typeahead search. */ - protected searchTerm = computed(() => this.label() ?? this.element().textContent); + protected searchTerm = computed(() => this.label() ?? this.element.textContent); /** The parent Listbox UIPattern. */ protected listbox = computed(() => this._listbox._pattern); - /** A reference to the option element to be focused on navigation. */ - protected element = computed(() => this._elementRef.nativeElement); - /** The value of the option. */ value = input.required(); @@ -271,7 +274,7 @@ export class Option { id: this.id, value: this.value, listbox: this.listbox, - element: this.element, + element: () => this.element, searchTerm: this.searchTerm, }); } diff --git a/src/aria/menu/menu.ts b/src/aria/menu/menu.ts index c7b113d874cf..4046446ddffb 100644 --- a/src/aria/menu/menu.ts +++ b/src/aria/menu/menu.ts @@ -69,15 +69,15 @@ import {Directionality} from '@angular/cdk/bidi'; }, }) export class MenuTrigger { - /** A reference to the menu trigger element. */ + /** A reference to the host element. */ private readonly _elementRef = inject(ElementRef); + /** A reference to the host element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; + /** The directionality (LTR / RTL) context for the application (or a subtree of it). */ readonly textDirection = inject(Directionality).valueSignal; - /** A reference to the menu element. */ - readonly element: HTMLButtonElement = this._elementRef.nativeElement; - /** The menu associated with the trigger. */ menu = input | undefined>(undefined); @@ -175,11 +175,11 @@ export class Menu { this._allItems().filter(i => i.parent === this), ); - /** A reference to the menu element. */ + /** A reference to the host element. */ private readonly _elementRef = inject(ElementRef); - /** A reference to the menu element. */ - readonly element: HTMLElement = this._elementRef.nativeElement; + /** A reference to the host element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; /** The directionality (LTR / RTL) context for the application (or a subtree of it). */ readonly textDirection = inject(Directionality).valueSignal; @@ -320,11 +320,11 @@ export class MenuBar { readonly _items: SignalLike[]> = () => this._allItems().filter(i => i.parent === this); - /** A reference to the menu element. */ + /** A reference to the host element. */ private readonly _elementRef = inject(ElementRef); - /** A reference to the menubar element. */ - readonly element: HTMLElement = this._elementRef.nativeElement; + /** A reference to the host element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; /** Whether the menubar is disabled. */ readonly disabled = input(false, {transform: booleanAttribute}); @@ -412,11 +412,11 @@ export class MenuBar { }, }) export class MenuItem { - /** A reference to the menu item element. */ + /** A reference to the host element. */ private readonly _elementRef = inject(ElementRef); - /** A reference to the menu element. */ - readonly element: HTMLElement = this._elementRef.nativeElement; + /** A reference to the host element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; /** The unique ID of the menu item. */ readonly id = input(inject(_IdGenerator).getId('ng-menu-item-', true)); diff --git a/src/aria/tabs/tabs.ts b/src/aria/tabs/tabs.ts index b925b050a62b..01de904e57d0 100644 --- a/src/aria/tabs/tabs.ts +++ b/src/aria/tabs/tabs.ts @@ -17,7 +17,6 @@ import { input, model, signal, - Signal, afterRenderEffect, OnInit, OnDestroy, @@ -31,14 +30,14 @@ import { } from '@angular/aria/private'; interface HasElement { - element: Signal; + element: HTMLElement; } /** * Sort directives by their document order. */ function sortDirectives(a: HasElement, b: HasElement) { - return (a.element().compareDocumentPosition(b.element()) & Node.DOCUMENT_POSITION_PRECEDING) > 0 + return (a.element.compareDocumentPosition(b.element) & Node.DOCUMENT_POSITION_PRECEDING) > 0 ? 1 : -1; } @@ -77,6 +76,12 @@ function sortDirectives(a: HasElement, b: HasElement) { exportAs: 'ngTabs', }) export class Tabs { + /** A reference to the host element. */ + private readonly _elementRef = inject(ElementRef); + + /** A reference to the host element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; + /** The TabList nested inside of the container. */ private readonly _tablist = signal(undefined); @@ -145,9 +150,12 @@ export class Tabs { }, }) export class TabList implements OnInit, OnDestroy { - /** A reference to the tab list element. */ + /** A reference to the host element. */ private readonly _elementRef = inject(ElementRef); + /** A reference to the host element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; + /** The parent Tabs. */ private readonly _tabs = inject(Tabs); @@ -283,9 +291,12 @@ export class TabList implements OnInit, OnDestroy { }, }) export class Tab implements HasElement, OnInit, OnDestroy { - /** A reference to the tab element. */ + /** A reference to the host element. */ private readonly _elementRef = inject(ElementRef); + /** A reference to the host element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; + /** The parent Tabs. */ private readonly _tabs = inject(Tabs); @@ -295,9 +306,6 @@ export class Tab implements HasElement, OnInit, OnDestroy { /** A unique identifier for the widget. */ readonly id = input(inject(_IdGenerator).getId('ng-tab-', true)); - /** The host native element. */ - readonly element = computed(() => this._elementRef.nativeElement); - /** The parent TabList UIPattern. */ readonly tablist = computed(() => this._tabList._pattern); @@ -324,6 +332,7 @@ export class Tab implements HasElement, OnInit, OnDestroy { tablist: this.tablist, tabpanel: this.tabpanel, expanded: signal(false), + element: () => this.element, }); /** Opens this tab panel. */ @@ -375,6 +384,12 @@ export class Tab implements HasElement, OnInit, OnDestroy { ], }) export class TabPanel implements OnInit, OnDestroy { + /** A reference to the host element. */ + private readonly _elementRef = inject(ElementRef); + + /** A reference to the host element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; + /** The DeferredContentAware host directive. */ private readonly _deferredContentAware = inject(DeferredContentAware); diff --git a/src/aria/toolbar/toolbar.ts b/src/aria/toolbar/toolbar.ts index a2b40d35b93c..e923c8daf1e8 100644 --- a/src/aria/toolbar/toolbar.ts +++ b/src/aria/toolbar/toolbar.ts @@ -15,7 +15,6 @@ import { input, booleanAttribute, signal, - Signal, OnInit, OnDestroy, contentChildren, @@ -30,14 +29,14 @@ import {Directionality} from '@angular/cdk/bidi'; import {_IdGenerator} from '@angular/cdk/a11y'; interface HasElement { - element: Signal; + element: HTMLElement; } /** * Sort directives by their document order. */ function sortDirectives(a: HasElement, b: HasElement) { - return (a.element().compareDocumentPosition(b.element()) & Node.DOCUMENT_POSITION_PRECEDING) > 0 + return (a.element.compareDocumentPosition(b.element) & Node.DOCUMENT_POSITION_PRECEDING) > 0 ? 1 : -1; } @@ -77,9 +76,12 @@ function sortDirectives(a: HasElement, b: HasElement) { }, }) export class Toolbar { - /** A reference to the toolbar element. */ + /** A reference to the host element. */ private readonly _elementRef = inject(ElementRef); + /** A reference to the host element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; + /** The TabList nested inside of the container. */ private readonly _widgets = signal(new Set>()); @@ -189,9 +191,12 @@ export class Toolbar { }, }) export class ToolbarWidget implements OnInit, OnDestroy { - /** A reference to the widget element. */ + /** A reference to the host element. */ private readonly _elementRef = inject(ElementRef); + /** A reference to the host element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; + /** The parent Toolbar. */ private readonly _toolbar = inject(Toolbar); @@ -201,9 +206,6 @@ export class ToolbarWidget implements OnInit, OnDestroy { /** The parent Toolbar UIPattern. */ readonly toolbar = computed(() => this._toolbar._pattern); - /** A reference to the widget element to be focused on navigation. */ - readonly element = computed(() => this._elementRef.nativeElement); - /** Whether the widget is disabled. */ readonly disabled = input(false, {transform: booleanAttribute}); @@ -230,7 +232,7 @@ export class ToolbarWidget implements OnInit, OnDestroy { ...this, id: this.id, value: this.value, - element: this.element, + element: () => this.element, }); ngOnInit() { @@ -253,6 +255,12 @@ export class ToolbarWidget implements OnInit, OnDestroy { exportAs: 'ngToolbarWidgetGroup', }) export class ToolbarWidgetGroup { + /** A reference to the host element. */ + private readonly _elementRef = inject(ElementRef); + + /** A reference to the host element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; + /** The parent Toolbar. */ private readonly _toolbar = inject(Toolbar, {optional: true}); diff --git a/src/aria/tree/tree.ts b/src/aria/tree/tree.ts index b3c0493cf900..264886880386 100644 --- a/src/aria/tree/tree.ts +++ b/src/aria/tree/tree.ts @@ -34,14 +34,14 @@ import { import {ComboboxPopup} from '../combobox'; interface HasElement { - element: Signal; + element: HTMLElement; } /** * Sort directives by their document order. */ function sortDirectives(a: HasElement, b: HasElement) { - return (a.element().compareDocumentPosition(b.element()) & Node.DOCUMENT_POSITION_PRECEDING) > 0 + return (a.element.compareDocumentPosition(b.element) & Node.DOCUMENT_POSITION_PRECEDING) > 0 ? 1 : -1; } @@ -98,23 +98,23 @@ function sortDirectives(a: HasElement, b: HasElement) { hostDirectives: [ComboboxPopup], }) export class Tree { + /** A reference to the host element. */ + private readonly _elementRef = inject(ElementRef); + + /** A reference to the host element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; + /** A reference to the parent combobox popup, if one exists. */ private readonly _popup = inject>(ComboboxPopup, { optional: true, }); - /** A reference to the tree element. */ - private readonly _elementRef = inject(ElementRef); - /** All TreeItem instances within this tree. */ private readonly _unorderedItems = signal(new Set>()); /** A unique identifier for the tree. */ readonly id = input(inject(_IdGenerator).getId('ng-tree-', true)); - /** The host native element. */ - readonly element = computed(() => this._elementRef.nativeElement); - /** Orientation of the tree. */ readonly orientation = input<'vertical' | 'horizontal'>('vertical'); @@ -182,6 +182,7 @@ export class Tree { ), activeItem: signal | undefined>(undefined), combobox: () => this._popup?.combobox?._pattern, + element: () => this.element, }; this._pattern = this._popup?.combobox @@ -269,18 +270,18 @@ export class Tree { }, }) export class TreeItem extends DeferredContentAware implements OnInit, OnDestroy, HasElement { - /** A reference to the tree item element. */ + /** A reference to the host element. */ private readonly _elementRef = inject(ElementRef); + /** A reference to the host element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; + /** The owned tree item group. */ private readonly _group = signal | undefined>(undefined); /** A unique identifier for the tree item. */ readonly id = input(inject(_IdGenerator).getId('ng-tree-item-', true)); - /** The host native element. */ - readonly element = computed(() => this._elementRef.nativeElement); - /** The value of the tree item. */ readonly value = input.required(); @@ -300,7 +301,7 @@ export class TreeItem extends DeferredContentAware implements OnInit, OnDestr readonly label = input(); /** Search term for typeahead. */ - readonly searchTerm = computed(() => this.label() ?? this.element().textContent); + readonly searchTerm = computed(() => this.label() ?? this.element.textContent); /** The tree root. */ readonly tree: Signal> = computed(() => { @@ -362,6 +363,7 @@ export class TreeItem extends DeferredContentAware implements OnInit, OnDestr parent: parentPattern, children: computed(() => this._group()?.children() ?? []), hasChildren: computed(() => !!this._group()), + element: () => this.element, }); } @@ -405,6 +407,12 @@ export class TreeItem extends DeferredContentAware implements OnInit, OnDestr hostDirectives: [DeferredContent], }) export class TreeItemGroup implements OnInit, OnDestroy { + /** A reference to the host element. */ + private readonly _elementRef = inject(ElementRef); + + /** A reference to the host element. */ + readonly element = this._elementRef.nativeElement as HTMLElement; + /** The DeferredContent host directive. */ private readonly _deferredContent = inject(DeferredContent); diff --git a/src/components-examples/aria/combobox/combobox-dialog/combobox-dialog-example.ts b/src/components-examples/aria/combobox/combobox-dialog/combobox-dialog-example.ts index db6660a0fba8..fc0de32bcc93 100644 --- a/src/components-examples/aria/combobox/combobox-dialog/combobox-dialog-example.ts +++ b/src/components-examples/aria/combobox/combobox-dialog/combobox-dialog-example.ts @@ -80,14 +80,13 @@ export class ComboboxDialogExample { const combobox = this.combobox()!; const comboboxRect = combobox.inputElement()?.getBoundingClientRect(); - const dialogEl = dialog.element.nativeElement; const scrollY = window.scrollY; if (comboboxRect) { - dialogEl.style.width = `${comboboxRect.width}px`; - dialogEl.style.top = `${comboboxRect.bottom + scrollY + 4}px`; - dialogEl.style.left = `${comboboxRect.left - 1}px`; + dialog.element.style.width = `${comboboxRect.width}px`; + dialog.element.style.top = `${comboboxRect.bottom + scrollY + 4}px`; + dialog.element.style.left = `${comboboxRect.left - 1}px`; } } } diff --git a/src/components-examples/aria/combobox/combobox-readonly-multiselect/combobox-readonly-multiselect-example.ts b/src/components-examples/aria/combobox/combobox-readonly-multiselect/combobox-readonly-multiselect-example.ts index b99301a26245..4d1b9df06173 100644 --- a/src/components-examples/aria/combobox/combobox-readonly-multiselect/combobox-readonly-multiselect-example.ts +++ b/src/components-examples/aria/combobox/combobox-readonly-multiselect/combobox-readonly-multiselect-example.ts @@ -53,8 +53,7 @@ export class ComboboxReadonlyMultiselectExample { const combobox = this.combobox()!; combobox._pattern.expanded() ? this.showPopover() : popover.nativeElement.hidePopover(); - // TODO(wagnermaciel): Make this easier for developers to do. - this.listbox()?._pattern.inputs.activeItem()?.element()?.scrollIntoView({block: 'nearest'}); + this.listbox()?.scrollActiveItemIntoView(); }); } diff --git a/src/components-examples/aria/grid/grid-table/grid-chips.ts b/src/components-examples/aria/grid/grid-table/grid-chips.ts index cb2d67ac2db2..e0e421ba5d58 100644 --- a/src/components-examples/aria/grid/grid-table/grid-chips.ts +++ b/src/components-examples/aria/grid/grid-table/grid-chips.ts @@ -27,6 +27,6 @@ export class GridChips { } focus(): void { - this.firstCell()?.element().focus(); + this.firstCell()?.element.focus(); } }