diff --git a/src/aria/accordion/accordion.ts b/src/aria/accordion/accordion.ts index 0b21b144578f..e136c7926383 100644 --- a/src/aria/accordion/accordion.ts +++ b/src/aria/accordion/accordion.ts @@ -47,7 +47,7 @@ import { 'role': 'region', '[attr.id]': '_pattern.id()', '[attr.aria-labelledby]': '_pattern.accordionTrigger()?.id()', - '[attr.inert]': '_pattern.hidden() ? true : null', + '[attr.inert]': '!visible() ? true : null', }, }) export class AccordionPanel { @@ -60,6 +60,9 @@ export class AccordionPanel { /** A local unique identifier for the panel, used to match with its trigger's value. */ value = input.required(); + /** Whether the accordion panel is visible. True if the associated trigger is expanded. */ + readonly visible = computed(() => !this._pattern.hidden()); + /** The parent accordion trigger pattern that controls this panel. This is set by AccordionGroup. */ readonly accordionTrigger: WritableSignal = signal(undefined); @@ -74,7 +77,7 @@ export class AccordionPanel { constructor() { // Connect the panel's hidden state to the DeferredContentAware's visibility. afterRenderEffect(() => { - this._deferredContentAware.contentVisible.set(!this._pattern.hidden()); + this._deferredContentAware.contentVisible.set(this.visible()); }); } } @@ -88,10 +91,10 @@ export class AccordionPanel { exportAs: 'ngAccordionTrigger', host: { 'class': 'ng-accordion-trigger', - '[attr.data-active]': '_pattern.active()', + '[attr.data-active]': 'active()', 'role': 'button', '[id]': '_pattern.id()', - '[attr.aria-expanded]': '_pattern.expanded()', + '[attr.aria-expanded]': 'expanded()', '[attr.aria-controls]': '_pattern.controls()', '[attr.aria-disabled]': '_pattern.disabled()', '[attr.disabled]': 'hardDisabled() ? true : null', @@ -117,6 +120,12 @@ export class AccordionTrigger { /** Whether the trigger is disabled. */ disabled = input(false, {transform: booleanAttribute}); + /** Whether the trigger is active. */ + readonly active = computed(() => this._pattern.active()); + + /** Whether the trigger is expanded. */ + readonly expanded = computed(() => this._pattern.expanded()); + /** * Whether this trigger is completely inaccessible. * diff --git a/src/aria/combobox/combobox.ts b/src/aria/combobox/combobox.ts index 8601227cbf8f..25f615e96451 100644 --- a/src/aria/combobox/combobox.ts +++ b/src/aria/combobox/combobox.ts @@ -69,12 +69,6 @@ export class Combobox { /** The filter mode for the combobox. */ filterMode = input<'manual' | 'auto-select' | 'highlight'>('manual'); - /** Whether the combobox is focused. */ - readonly isFocused = signal(false); - - /** Whether the combobox has received focus yet. */ - private _hasBeenFocused = signal(false); - /** Whether the combobox is disabled. */ readonly disabled = input(false); @@ -122,12 +116,6 @@ export class Combobox { this._deferredContentAware?.contentVisible.set(true); } }); - - afterRenderEffect(() => { - if (!this._hasBeenFocused() && this._pattern.isFocused()) { - this._hasBeenFocused.set(true); - } - }); } } diff --git a/src/aria/menu/menu.ts b/src/aria/menu/menu.ts index c56aa3abab3a..2d74f91c0221 100644 --- a/src/aria/menu/menu.ts +++ b/src/aria/menu/menu.ts @@ -45,13 +45,13 @@ import {Directionality} from '@angular/cdk/bidi'; host: { 'class': 'ng-menu-trigger', '[attr.tabindex]': '_pattern.tabIndex()', - '[attr.aria-haspopup]': '_pattern.hasPopup()', - '[attr.aria-expanded]': '_pattern.expanded()', + '[attr.aria-haspopup]': 'hasPopup()', + '[attr.aria-expanded]': 'expanded()', '[attr.aria-controls]': '_pattern.menu()?.id()', '(click)': '_pattern.onClick()', '(keydown)': '_pattern.onKeydown($event)', '(focusout)': '_pattern.onFocusOut($event)', - '(focusin)': 'onFocusIn()', + '(focusin)': '_pattern.onFocusIn()', }, }) export class MenuTrigger { @@ -67,8 +67,11 @@ export class MenuTrigger { /** The menu associated with the trigger. */ menu = input | undefined>(undefined); - /** Whether the menu item has been focused. */ - readonly hasBeenFocused = signal(false); + /** Whether the menu is expanded. */ + readonly expanded = computed(() => this._pattern.expanded()); + + /** Whether the menu trigger has a popup. */ + readonly hasPopup = computed(() => this._pattern.hasPopup()); /** The menu trigger ui pattern instance. */ _pattern: MenuTriggerPattern = new MenuTriggerPattern({ @@ -80,11 +83,6 @@ export class MenuTrigger { constructor() { effect(() => this.menu()?.parent.set(this)); } - - /** Marks the menu trigger as having been focused. */ - onFocusIn() { - this.hasBeenFocused.set(true); - } } /** @@ -110,7 +108,7 @@ export class MenuTrigger { 'role': 'menu', 'class': 'ng-menu', '[attr.id]': '_pattern.id()', - '[attr.data-visible]': '_pattern.isVisible()', + '[attr.data-visible]': 'isVisible()', '(keydown)': '_pattern.onKeydown($event)', '(mouseover)': '_pattern.onMouseOver($event)', '(mouseout)': '_pattern.onMouseOut($event)', @@ -171,7 +169,7 @@ export class Menu { readonly items = () => this._items().map(i => i._pattern); /** Whether the menu is visible. */ - isVisible = computed(() => this._pattern.isVisible()); + readonly isVisible = computed(() => this._pattern.isVisible()); /** A callback function triggered when a menu item is selected. */ onSelect = output(); @@ -199,7 +197,7 @@ export class Menu { this._deferredContentAware?.contentVisible.set(true); } else { this._deferredContentAware?.contentVisible.set( - this._pattern.isVisible() || !!this.parent()?.hasBeenFocused(), + this._pattern.isVisible() || !!this.parent()?._pattern.hasBeenFocused(), ); } }); @@ -333,11 +331,11 @@ export class MenuBar { host: { 'role': 'menuitem', 'class': 'ng-menu-item', - '(focusin)': 'onFocusIn()', + '(focusin)': '_pattern.onFocusIn()', '[attr.tabindex]': '_pattern.tabIndex()', - '[attr.data-active]': '_pattern.isActive()', - '[attr.aria-haspopup]': '_pattern.hasPopup()', - '[attr.aria-expanded]': '_pattern.expanded()', + '[attr.data-active]': 'isActive()', + '[attr.aria-haspopup]': 'hasPopup()', + '[attr.aria-expanded]': 'expanded()', '[attr.aria-disabled]': '_pattern.disabled()', '[attr.aria-controls]': '_pattern.submenu()?.id()', }, @@ -375,8 +373,14 @@ export class MenuItem { /** The submenu associated with the menu item. */ readonly submenu = input | undefined>(undefined); - /** Whether the menu item has been focused. */ - readonly hasBeenFocused = signal(false); + /** Whether the menu item is active. */ + readonly isActive = computed(() => this._pattern.isActive()); + + /** Whether the menu is expanded. */ + readonly expanded = computed(() => this._pattern.expanded()); + + /** Whether the menu item has a popup. */ + readonly hasPopup = computed(() => this._pattern.hasPopup()); /** The menu item ui pattern instance. */ readonly _pattern: MenuItemPattern = new MenuItemPattern({ @@ -392,11 +396,6 @@ export class MenuItem { constructor() { effect(() => this.submenu()?.parent.set(this)); } - - /** Marks the menu item as having been focused. */ - onFocusIn() { - this.hasBeenFocused.set(true); - } } /** Defers the rendering of the menu content. */ diff --git a/src/aria/private/menu/menu.ts b/src/aria/private/menu/menu.ts index f9b4cec3c921..230665f979ef 100644 --- a/src/aria/private/menu/menu.ts +++ b/src/aria/private/menu/menu.ts @@ -578,6 +578,9 @@ export class MenuTriggerPattern { /** Whether the menu is expanded. */ expanded = signal(false); + /** Whether the menu trigger has received focus. */ + hasBeenFocused = signal(false); + /** The role of the menu trigger. */ role = () => 'button'; @@ -614,6 +617,11 @@ export class MenuTriggerPattern { this.expanded() ? this.close() : this.open({first: true}); } + /** Handles focusin events for the menu trigger. */ + onFocusIn() { + this.hasBeenFocused.set(true); + } + /** Handles focusout events for the menu trigger. */ onFocusOut(event: FocusEvent) { const element = this.inputs.element(); @@ -679,6 +687,9 @@ export class MenuItemPattern implements ListItem { /** Whether the menu item is active. */ isActive = computed(() => this.inputs.parent()?.inputs.activeItem() === this); + /** Whether the menu item has received focus. */ + hasBeenFocused = signal(false); + /** The tab index of the menu item. */ tabIndex = computed(() => { if (this.submenu() && this.submenu()?.inputs.activeItem()) { @@ -756,4 +767,9 @@ export class MenuItemPattern implements ListItem { } } } + + /** Handles focusin events for the menu item. */ + onFocusIn() { + this.hasBeenFocused.set(true); + } } diff --git a/src/aria/private/tree/tree.ts b/src/aria/private/tree/tree.ts index eacc7326cef8..96e7002b009a 100644 --- a/src/aria/private/tree/tree.ts +++ b/src/aria/private/tree/tree.ts @@ -185,7 +185,7 @@ export class TreePattern { /** The root is always expanded. */ readonly expanded = () => true; - /** The roow is always visible. */ + /** The root is always visible. */ readonly visible = () => true; /** The tab index of the tree. */ diff --git a/src/aria/tabs/tabs.ts b/src/aria/tabs/tabs.ts index 9fc4bf5ff096..f663d4055197 100644 --- a/src/aria/tabs/tabs.ts +++ b/src/aria/tabs/tabs.ts @@ -230,10 +230,10 @@ export class TabList implements OnInit, OnDestroy { host: { 'role': 'tab', 'class': 'ng-tab', - '[attr.data-active]': '_pattern.active()', + '[attr.data-active]': 'active()', '[attr.id]': '_pattern.id()', '[attr.tabindex]': '_pattern.tabIndex()', - '[attr.aria-selected]': '_pattern.selected()', + '[attr.aria-selected]': 'selected()', '[attr.aria-disabled]': '_pattern.disabled()', '[attr.aria-controls]': '_pattern.controls()', }, @@ -268,6 +268,15 @@ export class Tab implements HasElement, OnInit, OnDestroy { /** A local unique identifier for the tab. */ readonly value = input.required(); + /** Whether the tab is active. */ + readonly active = computed(() => this._pattern.active()); + + /** Whether the tab is expanded. */ + readonly expanded = computed(() => this._pattern.expanded()); + + /** Whether the tab is selected. */ + readonly selected = computed(() => this._pattern.selected()); + /** The Tab UIPattern. */ readonly _pattern: TabPattern = new TabPattern({ ...this, @@ -302,7 +311,7 @@ export class Tab implements HasElement, OnInit, OnDestroy { 'class': 'ng-tabpanel', '[attr.id]': '_pattern.id()', '[attr.tabindex]': '_pattern.tabIndex()', - '[attr.inert]': '_pattern.hidden() ? true : null', + '[attr.inert]': '!visible() ? true : null', '[attr.aria-labelledby]': '_pattern.labelledBy()', }, hostDirectives: [ @@ -328,6 +337,9 @@ export class TabPanel implements OnInit, OnDestroy { /** A local unique identifier for the tabpanel. */ readonly value = input.required(); + /** Whether the tab panel is visible. */ + readonly visible = computed(() => !this._pattern.hidden()); + /** The TabPanel UIPattern. */ readonly _pattern: TabPanelPattern = new TabPanelPattern({ ...this, @@ -336,7 +348,7 @@ export class TabPanel implements OnInit, OnDestroy { }); constructor() { - afterRenderEffect(() => this._deferredContentAware.contentVisible.set(!this._pattern.hidden())); + afterRenderEffect(() => this._deferredContentAware.contentVisible.set(this.visible())); } ngOnInit() { diff --git a/src/aria/toolbar/toolbar.ts b/src/aria/toolbar/toolbar.ts index 3141286ace95..262c2f5c9e2d 100644 --- a/src/aria/toolbar/toolbar.ts +++ b/src/aria/toolbar/toolbar.ts @@ -168,7 +168,7 @@ export class Toolbar { exportAs: 'ngToolbarWidget', host: { 'class': 'ng-toolbar-widget', - '[attr.data-active]': '_pattern.active()', + '[attr.data-active]': 'active()', '[attr.tabindex]': '_pattern.tabIndex()', '[attr.inert]': 'hardDisabled() ? true : null', '[attr.disabled]': 'hardDisabled() ? true : null', diff --git a/src/aria/tree/tree.ts b/src/aria/tree/tree.ts index 6b876da5263c..a6dfa8445999 100644 --- a/src/aria/tree/tree.ts +++ b/src/aria/tree/tree.ts @@ -220,14 +220,14 @@ export class Tree { exportAs: 'ngTreeItem', host: { 'class': 'ng-treeitem', - '[attr.data-active]': '_pattern.active()', + '[attr.data-active]': 'active()', 'role': 'treeitem', '[id]': '_pattern.id()', - '[attr.aria-expanded]': '_pattern.expandable() ? _pattern.expanded() : null', - '[attr.aria-selected]': '_pattern.selected()', + '[attr.aria-expanded]': 'expanded()', + '[attr.aria-selected]': 'selected()', '[attr.aria-current]': '_pattern.current()', '[attr.aria-disabled]': '_pattern.disabled()', - '[attr.aria-level]': '_pattern.level()', + '[attr.aria-level]': 'level()', '[attr.aria-setsize]': '_pattern.setsize()', '[attr.aria-posinset]': '_pattern.posinset()', '[attr.tabindex]': '_pattern.tabIndex()', @@ -272,6 +272,23 @@ export class TreeItem extends DeferredContentAware implements OnInit, OnDestr return (this.parent() as TreeItemGroup).ownedBy().tree(); }); + /** Whether the item is active. */ + readonly active = computed(() => this._pattern.active()); + + /** Whether this item is currently expanded, returning null if not expandable. */ + readonly expanded = computed(() => + this._pattern.expandable() ? this._pattern.expanded() : null, + ); + + /** The level of the current item in a tree. */ + readonly level = computed(() => this._pattern.level()); + + /** Whether the item is selected. */ + readonly selected = computed(() => this._pattern.selected()); + + /** Whether this item is visible due to all of its parents being expanded. */ + readonly visible = computed(() => this._pattern.visible()); + /** The UI pattern for this item. */ _pattern: TreeItemPattern;