Skip to content

Commit 79b2337

Browse files
authored
fix(cdk-experimental/menu): move shared menu logic to base class (#24645)
* fix(cdk-experimental/menu): move shared menu logic to base class fix(cdk-experimental/menu) make trigger items behave like normal items when pressing left/right * fixup! fix(cdk-experimental/menu): move shared menu logic to base class * fixup! fix(cdk-experimental/menu): move shared menu logic to base class
1 parent 99f1f38 commit 79b2337

File tree

14 files changed

+334
-351
lines changed

14 files changed

+334
-351
lines changed

src/cdk-experimental/menu/menu-bar.ts

Lines changed: 34 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,24 @@
77
*/
88

99
import {
10-
Directive,
11-
Input,
12-
ContentChildren,
13-
QueryList,
1410
AfterContentInit,
15-
OnDestroy,
16-
Optional,
17-
NgZone,
11+
Directive,
1812
ElementRef,
1913
Inject,
14+
NgZone,
15+
OnDestroy,
16+
Optional,
2017
Self,
2118
} from '@angular/core';
2219
import {Directionality} from '@angular/cdk/bidi';
23-
import {FocusKeyManager, FocusOrigin} from '@angular/cdk/a11y';
24-
import {LEFT_ARROW, RIGHT_ARROW, UP_ARROW, DOWN_ARROW, ESCAPE, TAB} from '@angular/cdk/keycodes';
25-
import {takeUntil, mergeAll, mapTo, startWith, mergeMap, switchMap} from 'rxjs/operators';
26-
import {Subject, merge} from 'rxjs';
20+
import {DOWN_ARROW, ESCAPE, LEFT_ARROW, RIGHT_ARROW, TAB, UP_ARROW} from '@angular/cdk/keycodes';
21+
import {takeUntil} from 'rxjs/operators';
2722
import {CdkMenuGroup} from './menu-group';
28-
import {CDK_MENU, Menu} from './menu-interface';
29-
import {CdkMenuItem} from './menu-item';
30-
import {MenuStack, MenuStackItem, FocusNext, MENU_STACK} from './menu-stack';
23+
import {CDK_MENU} from './menu-interface';
24+
import {FocusNext, MENU_STACK, MenuStack} from './menu-stack';
3125
import {PointerFocusTracker} from './pointer-focus-tracker';
32-
import {MenuAim, MENU_AIM} from './menu-aim';
26+
import {MENU_AIM, MenuAim} from './menu-aim';
27+
import {CdkMenuBase} from './menu-base';
3328

3429
/**
3530
* Directive applied to an element which configures it as a MenuBar by setting the appropriate
@@ -44,8 +39,6 @@ import {MenuAim, MENU_AIM} from './menu-aim';
4439
'role': 'menubar',
4540
'class': 'cdk-menu-bar',
4641
'tabindex': '0',
47-
'[attr.aria-orientation]': 'orientation',
48-
'(focus)': 'focusFirstItem()',
4942
'(keydown)': '_handleKeyEvent($event)',
5043
},
5144
providers: [
@@ -54,60 +47,31 @@ import {MenuAim, MENU_AIM} from './menu-aim';
5447
{provide: MENU_STACK, useClass: MenuStack},
5548
],
5649
})
57-
export class CdkMenuBar extends CdkMenuGroup implements Menu, AfterContentInit, OnDestroy {
58-
/**
59-
* Sets the aria-orientation attribute and determines where menus will be opened.
60-
* Does not affect styling/layout.
61-
*/
62-
@Input('cdkMenuBarOrientation') orientation: 'horizontal' | 'vertical' = 'horizontal';
63-
64-
/** Handles keyboard events for the MenuBar. */
65-
private _keyManager: FocusKeyManager<CdkMenuItem>;
66-
67-
/** Manages items under mouse focus */
68-
private _pointerTracker?: PointerFocusTracker<CdkMenuItem>;
50+
export class CdkMenuBar extends CdkMenuBase implements AfterContentInit, OnDestroy {
51+
override readonly orientation: 'horizontal' | 'vertical' = 'horizontal';
6952

70-
/** Emits when the MenuBar is destroyed. */
71-
private readonly _destroyed: Subject<void> = new Subject();
72-
73-
/** All child MenuItem elements nested in this MenuBar. */
74-
@ContentChildren(CdkMenuItem, {descendants: true})
75-
private readonly _allItems: QueryList<CdkMenuItem>;
76-
77-
/** The Menu Item which triggered the open submenu. */
78-
private _openItem?: CdkMenuItem;
53+
override menuStack: MenuStack;
7954

8055
constructor(
8156
private readonly _ngZone: NgZone,
82-
readonly _elementRef: ElementRef<HTMLElement>,
83-
@Inject(MENU_STACK) readonly _menuStack: MenuStack,
57+
elementRef: ElementRef<HTMLElement>,
58+
@Inject(MENU_STACK) menuStack: MenuStack,
8459
@Self() @Optional() @Inject(MENU_AIM) private readonly _menuAim?: MenuAim,
85-
@Optional() private readonly _dir?: Directionality,
60+
@Optional() dir?: Directionality,
8661
) {
87-
super();
62+
super(elementRef, menuStack, dir);
8863
}
8964

9065
override ngAfterContentInit() {
9166
super.ngAfterContentInit();
92-
93-
this._setKeyManager();
94-
this._subscribeToMenuOpen();
95-
this._subscribeToMenuStack();
67+
this._subscribeToMenuStackEmptied();
9668
this._subscribeToMouseManager();
97-
98-
this._menuAim?.initialize(this, this._pointerTracker!);
99-
}
100-
101-
/** Place focus on the first MenuItem in the menu and set the focus origin. */
102-
focusFirstItem(focusOrigin: FocusOrigin = 'program') {
103-
this._keyManager.setFocusOrigin(focusOrigin);
104-
this._keyManager.setFirstItemActive();
69+
this._menuAim?.initialize(this, this.pointerTracker!);
10570
}
10671

107-
/** Place focus on the last MenuItem in the menu and set the focus origin. */
108-
focusLastItem(focusOrigin: FocusOrigin = 'program') {
109-
this._keyManager.setFocusOrigin(focusOrigin);
110-
this._keyManager.setLastItemActive();
72+
override ngOnDestroy() {
73+
super.ngOnDestroy();
74+
this.pointerTracker?.destroy();
11175
}
11276

11377
/**
@@ -116,7 +80,7 @@ export class CdkMenuBar extends CdkMenuGroup implements Menu, AfterContentInit,
11680
* @param event the KeyboardEvent to handle.
11781
*/
11882
_handleKeyEvent(event: KeyboardEvent) {
119-
const keyManager = this._keyManager;
83+
const keyManager = this.keyManager;
12084
switch (event.keyCode) {
12185
case UP_ARROW:
12286
case DOWN_ARROW:
@@ -127,8 +91,8 @@ export class CdkMenuBar extends CdkMenuGroup implements Menu, AfterContentInit,
12791
// up/down keys were clicked: if the current menu is open, close it then focus and open the
12892
// next menu.
12993
if (
130-
(this._isHorizontal() && horizontalArrows) ||
131-
(!this._isHorizontal() && !horizontalArrows)
94+
(this.isHorizontal() && horizontalArrows) ||
95+
(!this.isHorizontal() && !horizontalArrows)
13296
) {
13397
event.preventDefault();
13498

@@ -157,69 +121,27 @@ export class CdkMenuBar extends CdkMenuGroup implements Menu, AfterContentInit,
157121
}
158122
}
159123

160-
/** Setup the FocusKeyManager with the correct orientation for the menu bar. */
161-
private _setKeyManager() {
162-
this._keyManager = new FocusKeyManager(this._allItems)
163-
.withWrap()
164-
.withTypeAhead()
165-
.withHomeAndEnd();
166-
167-
if (this._isHorizontal()) {
168-
this._keyManager.withHorizontalOrientation(this._dir?.value || 'ltr');
169-
} else {
170-
this._keyManager.withVerticalOrientation();
171-
}
172-
}
173-
174124
/**
175125
* Set the PointerFocusTracker and ensure that when mouse focus changes the key manager is updated
176126
* with the latest menu item under mouse focus.
177127
*/
178128
private _subscribeToMouseManager() {
179129
this._ngZone.runOutsideAngular(() => {
180-
this._pointerTracker = new PointerFocusTracker(this._allItems);
181-
this._pointerTracker.entered.pipe(takeUntil(this._destroyed)).subscribe(item => {
182-
if (this._hasOpenSubmenu()) {
183-
this._keyManager.setActiveItem(item);
130+
this.pointerTracker = new PointerFocusTracker(this.items);
131+
this.pointerTracker.entered.pipe(takeUntil(this.destroyed)).subscribe(item => {
132+
if (this.hasOpenSubmenu()) {
133+
this.keyManager.setActiveItem(item);
184134
}
185135
});
186136
});
187137
}
188138

189-
/** Subscribe to the MenuStack close and empty observables. */
190-
private _subscribeToMenuStack() {
191-
this._menuStack.closed
192-
.pipe(takeUntil(this._destroyed))
193-
.subscribe(item => this._closeOpenMenu(item));
194-
195-
this._menuStack.emptied
196-
.pipe(takeUntil(this._destroyed))
197-
.subscribe(event => this._toggleOpenMenu(event));
198-
}
199-
200-
/**
201-
* Close the open menu if the current active item opened the requested MenuStackItem.
202-
* @param item the MenuStackItem requested to be closed.
203-
*/
204-
private _closeOpenMenu(menu: MenuStackItem | undefined) {
205-
const trigger = this._openItem;
206-
const keyManager = this._keyManager;
207-
if (menu === trigger?.getMenuTrigger()?.getMenu()) {
208-
trigger?.getMenuTrigger()?.closeMenu();
209-
// If the user has moused over a sibling item we want to focus the element under mouse focus
210-
// not the trigger which previously opened the now closed menu.
211-
if (trigger) {
212-
keyManager.setActiveItem(this._pointerTracker?.activeElement || trigger);
213-
}
214-
}
215-
}
216-
217139
/**
218140
* Set focus to either the current, previous or next item based on the FocusNext event, then
219141
* open the previous or next item.
220142
*/
221143
private _toggleOpenMenu(event: FocusNext | undefined) {
222-
const keyManager = this._keyManager;
144+
const keyManager = this.keyManager;
223145
switch (event) {
224146
case FocusNext.nextItem:
225147
keyManager.setFocusOrigin('keyboard');
@@ -242,48 +164,9 @@ export class CdkMenuBar extends CdkMenuGroup implements Menu, AfterContentInit,
242164
}
243165
}
244166

245-
/**
246-
* @return true if the menu bar is configured to be horizontal.
247-
*/
248-
private _isHorizontal() {
249-
return this.orientation === 'horizontal';
250-
}
251-
252-
/**
253-
* Subscribe to the menu trigger's open events in order to track the trigger which opened the menu
254-
* and stop tracking it when the menu is closed.
255-
*/
256-
private _subscribeToMenuOpen() {
257-
const exitCondition = merge(this._allItems.changes, this._destroyed);
258-
this._allItems.changes
259-
.pipe(
260-
startWith(this._allItems),
261-
mergeMap((list: QueryList<CdkMenuItem>) =>
262-
list
263-
.filter(item => item.hasMenu())
264-
.map(item => item.getMenuTrigger()!.opened.pipe(mapTo(item), takeUntil(exitCondition))),
265-
),
266-
mergeAll(),
267-
switchMap((item: CdkMenuItem) => {
268-
this._openItem = item;
269-
return item.getMenuTrigger()!.closed;
270-
}),
271-
takeUntil(this._destroyed),
272-
)
273-
.subscribe(() => (this._openItem = undefined));
274-
}
275-
276-
/** Return true if the MenuBar has an open submenu. */
277-
private _hasOpenSubmenu() {
278-
return !!this._openItem;
279-
}
280-
281-
override ngOnDestroy() {
282-
super.ngOnDestroy();
283-
284-
this._destroyed.next();
285-
this._destroyed.complete();
286-
287-
this._pointerTracker?.destroy();
167+
private _subscribeToMenuStackEmptied() {
168+
this.menuStack?.emptied
169+
.pipe(takeUntil(this.destroyed))
170+
.subscribe(event => this._toggleOpenMenu(event));
288171
}
289172
}

0 commit comments

Comments
 (0)