diff --git a/goldens/aria/menu/index.api.md b/goldens/aria/menu/index.api.md index 71c20bf93c87..cbf1b7028328 100644 --- a/goldens/aria/menu/index.api.md +++ b/goldens/aria/menu/index.api.md @@ -85,11 +85,12 @@ export class MenuItem implements OnInit, OnDestroy { open(): void; readonly parent: Menu | MenuBar | null; readonly _pattern: MenuItemPattern; + readonly role: _angular_core.InputSignal<"menuitem" | "menuitemradio" | "menuitemcheckbox">; readonly searchTerm: _angular_core.ModelSignal; readonly submenu: _angular_core.InputSignal | undefined>; readonly value: _angular_core.InputSignal; // (undocumented) - static ɵdir: _angular_core.ɵɵDirectiveDeclaration, "[ngMenuItem]", ["ngMenuItem"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": true; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "searchTerm": { "alias": "searchTerm"; "required": false; "isSignal": true; }; "submenu": { "alias": "submenu"; "required": false; "isSignal": true; }; }, { "searchTerm": "searchTermChange"; }, never, never, true, never>; + static ɵdir: _angular_core.ɵɵDirectiveDeclaration, "[ngMenuItem]", ["ngMenuItem"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": true; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "searchTerm": { "alias": "searchTerm"; "required": false; "isSignal": true; }; "role": { "alias": "role"; "required": false; "isSignal": true; }; "submenu": { "alias": "submenu"; "required": false; "isSignal": true; }; }, { "searchTerm": "searchTermChange"; }, never, never, true, never>; // (undocumented) static ɵfac: _angular_core.ɵɵFactoryDeclaration, never>; } diff --git a/goldens/aria/private/index.api.md b/goldens/aria/private/index.api.md index dd477e85709d..e5143c7b9222 100644 --- a/goldens/aria/private/index.api.md +++ b/goldens/aria/private/index.api.md @@ -388,6 +388,7 @@ export interface MenuInputs extends Omit, V>, ' // @public export interface MenuItemInputs extends Omit, 'index' | 'selectable'> { parent: SignalLike | MenuBarPattern | undefined>; + role: SignalLike<'menuitem' | 'menuitemradio' | 'menuitemcheckbox'>; submenu: SignalLike | undefined>; } @@ -414,7 +415,7 @@ export class MenuItemPattern implements ListItem { first?: boolean; last?: boolean; }): void; - readonly role: () => string; + readonly role: () => "menuitem" | "menuitemradio" | "menuitemcheckbox"; readonly searchTerm: SignalLike; readonly selectable: SignalLike; readonly submenu: SignalLike | undefined>; diff --git a/src/aria/menu/menu-item.ts b/src/aria/menu/menu-item.ts index 958de1a1ec69..f19e11a25e82 100644 --- a/src/aria/menu/menu-item.ts +++ b/src/aria/menu/menu-item.ts @@ -44,7 +44,7 @@ import type {MenuBar} from './menu-bar'; selector: '[ngMenuItem]', exportAs: 'ngMenuItem', host: { - 'role': 'menuitem', + '[attr.role]': '_pattern.role()', '(focusin)': '_pattern.onFocusIn()', '[attr.tabindex]': '_pattern.tabIndex()', '[attr.data-active]': 'active()', @@ -73,6 +73,9 @@ export class MenuItem implements OnInit, OnDestroy { /** The search term associated with the menu item. */ readonly searchTerm = model(''); + /** The role of the menu item. */ + readonly role = input<'menuitem' | 'menuitemradio' | 'menuitemcheckbox'>('menuitem'); + /** A reference to the parent menu or menubar. */ readonly parent = inject | MenuBar>(MENU_COMPONENT, {optional: true}); @@ -97,6 +100,7 @@ export class MenuItem implements OnInit, OnDestroy { searchTerm: this.searchTerm, parent: computed(() => this.parent?._pattern), submenu: computed(() => this.submenu()?._pattern), + role: this.role, }); constructor() { diff --git a/src/aria/menu/menu.spec.ts b/src/aria/menu/menu.spec.ts index 5030ec322f6d..df717dcef825 100644 --- a/src/aria/menu/menu.spec.ts +++ b/src/aria/menu/menu.spec.ts @@ -498,6 +498,28 @@ describe('Standalone Menu Pattern', () => { expect(item?.getAttribute('aria-label')).toBe('Apple item label'); }); + describe('role override', () => { + it('should allow overriding the default menuitem role', () => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + imports: [MenuItemRoleOverrideExample], + }); + const roleFixture = TestBed.createComponent(MenuItemRoleOverrideExample); + roleFixture.detectChanges(); + + const items = roleFixture.debugElement + .queryAll(By.directive(MenuItem)) + .map(debugEl => debugEl.nativeElement as HTMLElement); + + expect(items[0].getAttribute('role')).toBe('menuitemradio'); + expect(items[1].getAttribute('role')).toBe('menuitemcheckbox'); + + roleFixture.componentInstance.customRole.set('menuitem'); + roleFixture.detectChanges(); + expect(items[1].getAttribute('role')).toBe('menuitem'); + }); + }); + describe('structural validations', () => { let consoleSpy: jasmine.Spy; @@ -1227,3 +1249,17 @@ class MenuWithDuplicateValues {} changeDetection: ChangeDetectionStrategy.Eager, }) class MenuItemOutsideMenu {} + +@Component({ + template: ` +
+
Item 0
+
Item 1
+
+ `, + imports: [Menu, MenuItem], + changeDetection: ChangeDetectionStrategy.Eager, +}) +class MenuItemRoleOverrideExample { + customRole = signal<'menuitem' | 'menuitemradio' | 'menuitemcheckbox'>('menuitemcheckbox'); +} diff --git a/src/aria/private/menu/menu.spec.ts b/src/aria/private/menu/menu.spec.ts index b564a085e254..8d7e3e357bac 100644 --- a/src/aria/private/menu/menu.spec.ts +++ b/src/aria/private/menu/menu.spec.ts @@ -80,6 +80,7 @@ function getMenuBarPattern(values: string[], opts?: {textDirection: 'ltr' | 'rtl parent: signal(menubar), element: signal(element), submenu: signal(undefined), + role: signal('menuitem'), }) as TestMenuItem; }), ); @@ -125,6 +126,7 @@ function getMenuPattern( parent: signal(menu), element: signal(element), submenu: signal(undefined), + role: signal('menuitem'), }) as TestMenuItem; }), ); diff --git a/src/aria/private/menu/menu.ts b/src/aria/private/menu/menu.ts index 1a3ebb877610..b11713215d35 100644 --- a/src/aria/private/menu/menu.ts +++ b/src/aria/private/menu/menu.ts @@ -65,6 +65,9 @@ export interface MenuItemInputs extends Omit, 'index' | 'selectab /** A reference to the submenu associated with the menu item. */ submenu: SignalLike | undefined>; + + /** The role of the menu item. */ + role: SignalLike<'menuitem' | 'menuitemradio' | 'menuitemcheckbox'>; } /** The menu ui pattern class. */ @@ -778,7 +781,7 @@ export class MenuItemPattern implements ListItem { readonly controls = signal(undefined); /** The role of the menu item. */ - readonly role = () => 'menuitem'; + readonly role = () => this.inputs.role(); /** Whether the menu item has a popup. */ readonly hasPopup = computed(() => !!this.submenu());