Skip to content

Commit 9b997de

Browse files
committed
fix(cdk-experimental/menu): move shared menu logic to base class
1 parent b4058d7 commit 9b997de

File tree

6 files changed

+248
-302
lines changed

6 files changed

+248
-302
lines changed

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

Lines changed: 41 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,27 @@
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';
28+
29+
// TODO(mmalerba): Keyboard controls should match
30+
// https://www.w3.org/TR/wai-aria-practices-1.1/examples/menubar/menubar-1/menubar-1.html#
3331

3432
/**
3533
* Directive applied to an element which configures it as a MenuBar by setting the appropriate
@@ -54,60 +52,32 @@ import {MenuAim, MENU_AIM} from './menu-aim';
5452
{provide: MENU_STACK, useClass: MenuStack},
5553
],
5654
})
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>;
55+
export class CdkMenuBar extends CdkMenuBase implements AfterContentInit, OnDestroy {
56+
override orientation: 'horizontal' | 'vertical' = 'horizontal';
6657

67-
/** Manages items under mouse focus */
68-
private _pointerTracker?: PointerFocusTracker<CdkMenuItem>;
69-
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;
58+
override menuStack: MenuStack;
7959

8060
constructor(
8161
private readonly _ngZone: NgZone,
82-
readonly _elementRef: ElementRef<HTMLElement>,
83-
@Inject(MENU_STACK) readonly _menuStack: MenuStack,
62+
elementRef: ElementRef<HTMLElement>,
63+
@Inject(MENU_STACK) menuStack: MenuStack,
8464
@Self() @Optional() @Inject(MENU_AIM) private readonly _menuAim?: MenuAim,
85-
@Optional() private readonly _dir?: Directionality,
65+
@Optional() dir?: Directionality,
8666
) {
87-
super();
67+
super(elementRef, menuStack, dir);
8868
}
8969

9070
override ngAfterContentInit() {
9171
super.ngAfterContentInit();
92-
93-
this._setKeyManager();
94-
this._subscribeToMenuOpen();
95-
this._subscribeToMenuStack();
72+
this._subscribeToMenuStackEmptied();
9673
this._subscribeToMouseManager();
9774

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();
75+
this._menuAim?.initialize(this, this.pointerTracker!);
10576
}
10677

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();
78+
override ngOnDestroy() {
79+
super.ngOnDestroy();
80+
this.pointerTracker?.destroy();
11181
}
11282

11383
/**
@@ -116,7 +86,7 @@ export class CdkMenuBar extends CdkMenuGroup implements Menu, AfterContentInit,
11686
* @param event the KeyboardEvent to handle.
11787
*/
11888
_handleKeyEvent(event: KeyboardEvent) {
119-
const keyManager = this._keyManager;
89+
const keyManager = this.keyManager;
12090
switch (event.keyCode) {
12191
case UP_ARROW:
12292
case DOWN_ARROW:
@@ -127,8 +97,8 @@ export class CdkMenuBar extends CdkMenuGroup implements Menu, AfterContentInit,
12797
// up/down keys were clicked: if the current menu is open, close it then focus and open the
12898
// next menu.
12999
if (
130-
(this._isHorizontal() && horizontalArrows) ||
131-
(!this._isHorizontal() && !horizontalArrows)
100+
(this.isHorizontal() && horizontalArrows) ||
101+
(!this.isHorizontal() && !horizontalArrows)
132102
) {
133103
event.preventDefault();
134104

@@ -157,73 +127,35 @@ export class CdkMenuBar extends CdkMenuGroup implements Menu, AfterContentInit,
157127
}
158128
}
159129

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-
174130
/**
175131
* Set the PointerFocusTracker and ensure that when mouse focus changes the key manager is updated
176132
* with the latest menu item under mouse focus.
177133
*/
178134
private _subscribeToMouseManager() {
179135
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);
136+
this.pointerTracker = new PointerFocusTracker(this.items);
137+
this.pointerTracker.entered.pipe(takeUntil(this.destroyed)).subscribe(item => {
138+
// TODO(mmalerba): Why does the menu bar do this in a hasOpenSubmenu check
139+
// when menu just does it regardless?
140+
if (this.hasOpenSubmenu()) {
141+
this.keyManager.setActiveItem(item);
184142
}
185143
});
186144
});
187145
}
188146

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-
217147
/**
218148
* Set focus to either the current, previous or next item based on the FocusNext event, then
219149
* open the previous or next item.
220150
*/
221151
private _toggleOpenMenu(event: FocusNext | undefined) {
222-
const keyManager = this._keyManager;
152+
const keyManager = this.keyManager;
223153
switch (event) {
224154
case FocusNext.nextItem:
225155
keyManager.setFocusOrigin('keyboard');
226156
keyManager.setNextItemActive();
157+
// TODO(mmalerba): This seems similar to what menu does, but menu bar also calls openMenu
158+
// figure out why this is different.
227159
keyManager.activeItem?.getMenuTrigger()?.openMenu();
228160
break;
229161

@@ -242,48 +174,9 @@ export class CdkMenuBar extends CdkMenuGroup implements Menu, AfterContentInit,
242174
}
243175
}
244176

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();
177+
private _subscribeToMenuStackEmptied() {
178+
this.menuStack?.emptied
179+
.pipe(takeUntil(this.destroyed))
180+
.subscribe(event => this._toggleOpenMenu(event));
288181
}
289182
}

0 commit comments

Comments
 (0)