Skip to content

Commit 5d1d36c

Browse files
committed
fix(cdk-experimental/menu): stop inline menu triggers capturing focus
1 parent 9531b68 commit 5d1d36c

File tree

4 files changed

+37
-14
lines changed

4 files changed

+37
-14
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ import {CdkMenuBase} from './menu-base';
3838
host: {
3939
'role': 'menubar',
4040
'class': 'cdk-menu-bar',
41-
'tabindex': '0',
4241
'(keydown)': '_handleKeyEvent($event)',
4342
},
4443
providers: [
@@ -52,6 +51,8 @@ export class CdkMenuBar extends CdkMenuBase implements AfterContentInit, OnDestr
5251

5352
override menuStack: MenuStack;
5453

54+
override _isInline = true;
55+
5556
constructor(
5657
private readonly _ngZone: NgZone,
5758
elementRef: ElementRef<HTMLElement>,

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@ import {PointerFocusTracker} from './pointer-focus-tracker';
2828

2929
@Directive({
3030
host: {
31+
'[tabindex]': '_isInline ? (_hasFocus ? -1 : 0) : null',
3132
'[attr.aria-orientation]': 'orientation',
3233
'(focus)': 'focusFirstItem()',
34+
'(focusin)': 'menuStack.setHasFocus(true)',
35+
'(focusout)': 'menuStack.setHasFocus(false)',
3336
},
3437
})
3538
export abstract class CdkMenuBase
@@ -42,6 +45,10 @@ export abstract class CdkMenuBase
4245
*/
4346
orientation: 'horizontal' | 'vertical' = 'vertical';
4447

48+
_isInline = false;
49+
50+
_hasFocus = false;
51+
4552
/** All child MenuItem elements nested in this Menu. */
4653
@ContentChildren(CdkMenuItem, {descendants: true})
4754
protected readonly items: QueryList<CdkMenuItem>;
@@ -69,6 +76,7 @@ export abstract class CdkMenuBase
6976
override ngAfterContentInit() {
7077
super.ngAfterContentInit();
7178
this._setKeyManager();
79+
this._subscribeToHasFocus();
7280
this._subscribeToMenuOpen();
7381
this._subscribeToMenuStackClosed();
7482
}
@@ -163,4 +171,12 @@ export abstract class CdkMenuBase
163171
.pipe(takeUntil(this.destroyed))
164172
.subscribe(({item, focusParentMenu}) => this.closeOpenMenu(item, focusParentMenu));
165173
}
174+
175+
private _subscribeToHasFocus() {
176+
if (this._isInline) {
177+
this.menuStack.hasFocus.pipe(takeUntil(this.destroyed)).subscribe(hasFocus => {
178+
this._hasFocus = hasFocus;
179+
});
180+
}
181+
}
166182
}

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {Inject, Injectable, InjectionToken, Optional, SkipSelf} from '@angular/core';
1010
import {Observable, Subject} from 'rxjs';
11+
import {debounceTime, distinctUntilChanged, startWith} from 'rxjs/operators';
1112

1213
/** Events to emit as specified by the caller once the MenuStack is empty. */
1314
export const enum FocusNext {
@@ -58,14 +59,22 @@ export class MenuStack {
5859
private readonly _elements: MenuStackItem[] = [];
5960

6061
/** Emits the element which was popped off of the stack when requested by a closer. */
61-
private readonly _close: Subject<MenuStackCloseEvent> = new Subject();
62+
private readonly _close = new Subject<MenuStackCloseEvent>();
6263

6364
/** Emits once the MenuStack has become empty after popping off elements. */
64-
private readonly _empty: Subject<FocusNext | undefined> = new Subject();
65+
private readonly _empty = new Subject<FocusNext | undefined>();
66+
67+
private readonly _hasFocus = new Subject<boolean>();
6568

6669
/** Observable which emits the MenuStackItem which has been requested to close. */
6770
readonly closed: Observable<MenuStackCloseEvent> = this._close;
6871

72+
readonly hasFocus: Observable<boolean> = this._hasFocus.pipe(
73+
startWith(false),
74+
debounceTime(0),
75+
distinctUntilChanged(),
76+
);
77+
6978
/**
7079
* Observable which emits when the MenuStack is empty after popping off the last element. It
7180
* emits a FocusNext event which specifies the action the closer has requested the listener
@@ -162,4 +171,8 @@ export class MenuStack {
162171
hasInlineMenu() {
163172
return this._hasInlineMenu;
164173
}
174+
175+
setHasFocus(hasFocus: boolean) {
176+
this._hasFocus.next(hasFocus);
177+
}
165178
}

src/cdk-experimental/menu/menu.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@ import {CdkMenuBase} from './menu-base';
5757
host: {
5858
'role': 'menu',
5959
'class': 'cdk-menu',
60-
'[tabindex]': '_isInline() ? 0 : null',
61-
'[class.cdk-menu-inline]': '_isInline()',
60+
'[class.cdk-menu-inline]': '_isInline',
6261
'(keydown)': '_handleKeyEvent($event)',
6362
},
6463
providers: [
@@ -75,6 +74,8 @@ export class CdkMenu extends CdkMenuBase implements AfterContentInit, OnDestroy
7574
@ContentChildren(CdkMenuGroup, {descendants: true})
7675
private readonly _nestedGroups: QueryList<CdkMenuGroup>;
7776

77+
override _isInline = !this._parentTrigger;
78+
7879
constructor(
7980
private readonly _ngZone: NgZone,
8081
elementRef: ElementRef<HTMLElement>,
@@ -85,7 +86,7 @@ export class CdkMenu extends CdkMenuBase implements AfterContentInit, OnDestroy
8586
) {
8687
super(elementRef, menuStack, dir);
8788
this.destroyed.subscribe(this.closed);
88-
if (!this._isInline()) {
89+
if (!this._isInline) {
8990
this.menuStack.push(this);
9091
}
9192
this._parentTrigger?.registerChildMenu(this);
@@ -200,14 +201,6 @@ export class CdkMenu extends CdkMenuBase implements AfterContentInit, OnDestroy
200201
}
201202
}
202203

203-
/**
204-
* Return true if this menu is an inline menu. That is, it does not exist in a pop-up and is
205-
* always visible in the dom.
206-
*/
207-
_isInline() {
208-
return !this._parentTrigger;
209-
}
210-
211204
private _subscribeToMenuStackEmptied() {
212205
this.menuStack.emptied
213206
.pipe(takeUntil(this.destroyed))

0 commit comments

Comments
 (0)