Skip to content

Commit b4b8b20

Browse files
authored
fix(cdk-experimental/menu): fix issue where left/right on standalone menu closes it (#24696)
* refactor(cdk-experimental/menu): make MenuStack required * fix(cdk-experimental/menu): fix issue where left/right on standalone menu closes it also consolidates some shared trigger logic under the common base class * fixup! fix(cdk-experimental/menu): fix issue where left/right on standalone menu closes it * fixup! fix(cdk-experimental/menu): fix issue where left/right on standalone menu closes it * fixup! fix(cdk-experimental/menu): fix issue where left/right on standalone menu closes it
1 parent 6fc7c50 commit b4b8b20

16 files changed

+194
-178
lines changed

src/cdk-experimental/menu/context-menu.spec.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import {CdkMenuModule} from './menu-module';
33
import {TestBed, waitForAsync, ComponentFixture} from '@angular/core/testing';
44
import {CdkMenu} from './menu';
55
import {CdkContextMenuTrigger} from './context-menu';
6-
import {dispatchMouseEvent} from '../../cdk/testing/private';
6+
import {dispatchKeyboardEvent, dispatchMouseEvent} from '../../cdk/testing/private';
77
import {By} from '@angular/platform-browser';
88
import {CdkMenuItem} from './menu-item';
99
import {CdkMenuItemTrigger} from './menu-item-trigger';
1010
import {CdkMenuBar} from './menu-bar';
11+
import {LEFT_ARROW, RIGHT_ARROW} from '@angular/cdk/keycodes';
1112

1213
describe('CdkContextMenuTrigger', () => {
1314
describe('with simple context menu trigger', () => {
@@ -126,6 +127,22 @@ describe('CdkContextMenuTrigger', () => {
126127
openContextMenu();
127128
expect(document.activeElement!.id).toEqual('first_menu_item');
128129
});
130+
131+
it('should not close context menu when pressing left arrow', () => {
132+
openContextMenu();
133+
expect(document.activeElement!.id).toEqual('first_menu_item');
134+
dispatchKeyboardEvent(document.activeElement!, 'keydown', LEFT_ARROW, 'ArrowLeft');
135+
fixture.detectChanges();
136+
expect(getContextMenu()).toBeDefined();
137+
});
138+
139+
it('should not close context menu when pressing right arrow', () => {
140+
openContextMenu();
141+
expect(document.activeElement!.id).toEqual('first_menu_item');
142+
dispatchKeyboardEvent(document.activeElement!, 'keydown', RIGHT_ARROW, 'ArrowRight');
143+
fixture.detectChanges();
144+
expect(getContextMenu()).toBeDefined();
145+
});
129146
});
130147

131148
describe('nested context menu triggers', () => {

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

Lines changed: 3 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,24 @@
88

99
import {
1010
Directive,
11-
EventEmitter,
1211
Inject,
1312
Injectable,
1413
Injector,
1514
Input,
1615
OnDestroy,
1716
Optional,
18-
Output,
19-
TemplateRef,
2017
ViewContainerRef,
2118
} from '@angular/core';
2219
import {Directionality} from '@angular/cdk/bidi';
2320
import {
24-
ConnectedPosition,
2521
FlexibleConnectedPositionStrategy,
2622
Overlay,
2723
OverlayConfig,
28-
OverlayRef,
2924
STANDARD_DROPDOWN_BELOW_POSITIONS,
3025
} from '@angular/cdk/overlay';
3126
import {Portal, TemplatePortal} from '@angular/cdk/portal';
3227
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
33-
import {merge, partition, Subject} from 'rxjs';
28+
import {merge, partition} from 'rxjs';
3429
import {skip, takeUntil} from 'rxjs/operators';
3530
import {MENU_STACK, MenuStack} from './menu-stack';
3631
import {isClickInsideMenuOverlay} from './menu-item-trigger';
@@ -76,25 +71,14 @@ export type ContextMenuCoordinates = {x: number; y: number};
7671
host: {
7772
'(contextmenu)': '_openOnContextMenu($event)',
7873
},
74+
inputs: ['_menuTemplateRef: cdkContextMenuTriggerFor', 'menuPosition: cdkContextMenuPosition'],
75+
outputs: ['opened: cdkContextMenuOpened', 'closed: cdkContextMenuClosed'],
7976
providers: [
8077
{provide: MENU_TRIGGER, useExisting: CdkContextMenuTrigger},
8178
{provide: MENU_STACK, useClass: MenuStack},
8279
],
8380
})
8481
export class CdkContextMenuTrigger extends MenuTrigger implements OnDestroy {
85-
/** Template reference variable to the menu to open on right click. */
86-
@Input('cdkContextMenuTriggerFor')
87-
private _menuTemplateRef: TemplateRef<unknown>;
88-
89-
/** A list of preferred menu positions to be used when constructing the `FlexibleConnectedPositionStrategy` for this trigger's menu. */
90-
@Input('cdkMenuPosition') menuPosition: ConnectedPosition[];
91-
92-
/** Emits when the attached menu is requested to open. */
93-
@Output('cdkContextMenuOpened') readonly opened: EventEmitter<void> = new EventEmitter();
94-
95-
/** Emits when the attached menu is requested to close. */
96-
@Output('cdkContextMenuClosed') readonly closed: EventEmitter<void> = new EventEmitter();
97-
9882
/** Whether the context menu should be disabled. */
9983
@Input('cdkContextMenuDisabled')
10084
get disabled(): boolean {
@@ -105,18 +89,6 @@ export class CdkContextMenuTrigger extends MenuTrigger implements OnDestroy {
10589
}
10690
private _disabled = false;
10791

108-
/** A reference to the overlay which manages the triggered menu. */
109-
private _overlayRef: OverlayRef | null = null;
110-
111-
/** The content of the menu panel opened by this trigger. */
112-
private _menuPortal: TemplatePortal;
113-
114-
/** Emits when the element is destroyed. */
115-
private readonly _destroyed = new Subject<void>();
116-
117-
/** Emits when the outside pointer events listener on the overlay should be stopped. */
118-
private readonly _stopOutsideClicksListener = merge(this.closed, this._destroyed);
119-
12092
constructor(
12193
injector: Injector,
12294
protected readonly _viewContainerRef: ViewContainerRef,
@@ -199,11 +171,6 @@ export class CdkContextMenuTrigger extends MenuTrigger implements OnDestroy {
199171
}
200172
}
201173

202-
/** Whether the attached menu is open. */
203-
isOpen() {
204-
return !!this._overlayRef?.hasAttached();
205-
}
206-
207174
/**
208175
* Get the configuration object used to create the overlay.
209176
* @param coordinates the location to place the opened menu
@@ -277,19 +244,4 @@ export class CdkContextMenuTrigger extends MenuTrigger implements OnDestroy {
277244
});
278245
}
279246
}
280-
281-
ngOnDestroy() {
282-
this._destroyOverlay();
283-
284-
this._destroyed.next();
285-
this._destroyed.complete();
286-
}
287-
288-
/** Destroy and unset the overlay reference it if exists. */
289-
private _destroyOverlay() {
290-
if (this._overlayRef) {
291-
this._overlayRef.dispose();
292-
this._overlayRef = null;
293-
}
294-
}
295247
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -509,13 +509,13 @@ describe('MenuBar', () => {
509509
.queryAll(By.directive(CdkMenuItem))[0]
510510
.injector.get(CdkMenuItem)
511511
.getMenuTrigger()!
512-
.openMenu();
512+
.open();
513513
detectChanges();
514514
fixture.debugElement
515515
.queryAll(By.directive(CdkMenuItem))[2]
516516
.injector.get(CdkMenuItem)
517517
.getMenuTrigger()!
518-
.openMenu();
518+
.open();
519519
detectChanges();
520520

521521
fileMenuNativeItems[0].focus();

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import {CdkMenuBase} from './menu-base';
4444
providers: [
4545
{provide: CdkMenuGroup, useExisting: CdkMenuBar},
4646
{provide: CDK_MENU, useExisting: CdkMenuBar},
47-
{provide: MENU_STACK, useClass: MenuStack},
47+
{provide: MENU_STACK, useFactory: () => MenuStack.inline()},
4848
],
4949
})
5050
export class CdkMenuBar extends CdkMenuBase implements AfterContentInit, OnDestroy {
@@ -97,23 +97,23 @@ export class CdkMenuBar extends CdkMenuBase implements AfterContentInit, OnDestr
9797
event.preventDefault();
9898

9999
const prevIsOpen = keyManager.activeItem?.isMenuOpen();
100-
keyManager.activeItem?.getMenuTrigger()?.closeMenu();
100+
keyManager.activeItem?.getMenuTrigger()?.close();
101101

102102
keyManager.setFocusOrigin('keyboard');
103103
keyManager.onKeydown(event);
104104
if (prevIsOpen) {
105-
keyManager.activeItem?.getMenuTrigger()?.openMenu();
105+
keyManager.activeItem?.getMenuTrigger()?.open();
106106
}
107107
}
108108
break;
109109

110110
case ESCAPE:
111111
event.preventDefault();
112-
keyManager.activeItem?.getMenuTrigger()?.closeMenu();
112+
keyManager.activeItem?.getMenuTrigger()?.close();
113113
break;
114114

115115
case TAB:
116-
keyManager.activeItem?.getMenuTrigger()?.closeMenu();
116+
keyManager.activeItem?.getMenuTrigger()?.close();
117117
break;
118118

119119
default:
@@ -146,13 +146,13 @@ export class CdkMenuBar extends CdkMenuBase implements AfterContentInit, OnDestr
146146
case FocusNext.nextItem:
147147
keyManager.setFocusOrigin('keyboard');
148148
keyManager.setNextItemActive();
149-
keyManager.activeItem?.getMenuTrigger()?.openMenu();
149+
keyManager.activeItem?.getMenuTrigger()?.open();
150150
break;
151151

152152
case FocusNext.previousItem:
153153
keyManager.setFocusOrigin('keyboard');
154154
keyManager.setPreviousItemActive();
155-
keyManager.activeItem?.getMenuTrigger()?.openMenu();
155+
keyManager.activeItem?.getMenuTrigger()?.open();
156156
break;
157157

158158
case FocusNext.currentItem:

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export abstract class CdkMenuBase
4040
* Sets the aria-orientation attribute and determines where menus will be opened.
4141
* Does not affect styling/layout.
4242
*/
43-
readonly orientation: 'horizontal' | 'vertical' = 'vertical';
43+
orientation: 'horizontal' | 'vertical' = 'vertical';
4444

4545
/** All child MenuItem elements nested in this Menu. */
4646
@ContentChildren(CdkMenuItem, {descendants: true})
@@ -60,7 +60,7 @@ export abstract class CdkMenuBase
6060

6161
protected constructor(
6262
readonly _elementRef: ElementRef<HTMLElement>,
63-
@Optional() @Inject(MENU_STACK) readonly menuStack?: MenuStack,
63+
@Inject(MENU_STACK) readonly menuStack: MenuStack,
6464
@Optional() protected readonly dir?: Directionality,
6565
) {
6666
super();
@@ -109,7 +109,7 @@ export abstract class CdkMenuBase
109109
const keyManager = this.keyManager;
110110
const trigger = this.openItem;
111111
if (menu === trigger?.getMenuTrigger()?.getMenu()) {
112-
trigger?.getMenuTrigger()?.closeMenu();
112+
trigger?.getMenuTrigger()?.close();
113113
// If the user has moused over a sibling item we want to focus the element under mouse focus
114114
// not the trigger which previously opened the now closed menu.
115115
if (trigger) {
@@ -155,7 +155,7 @@ export abstract class CdkMenuBase
155155

156156
/** Subscribe to the MenuStack close and empty observables. */
157157
private _subscribeToMenuStackClosed() {
158-
this.menuStack?.closed
158+
this.menuStack.closed
159159
.pipe(takeUntil(this.destroyed))
160160
.subscribe(item => this.closeOpenMenu(item));
161161
}

src/cdk-experimental/menu/menu-item-checkbox.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {CdkMenuModule} from './menu-module';
55
import {CdkMenuItemCheckbox} from './menu-item-checkbox';
66
import {CDK_MENU} from './menu-interface';
77
import {CdkMenu} from './menu';
8+
import {MENU_STACK, MenuStack} from '@angular/cdk-experimental/menu/menu-stack';
89

910
describe('MenuItemCheckbox', () => {
1011
let fixture: ComponentFixture<SingleCheckboxButton>;
@@ -17,6 +18,7 @@ describe('MenuItemCheckbox', () => {
1718
declarations: [SingleCheckboxButton],
1819
providers: [
1920
{provide: CDK_MENU, useClass: CdkMenu},
21+
{provide: MENU_STACK, useClass: MenuStack},
2022
// View engine can't figure out the ElementRef to inject so we need to provide a fake
2123
{provide: ElementRef, useValue: new ElementRef<null>(null)},
2224
],

src/cdk-experimental/menu/menu-item-radio.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {CdkMenuModule} from './menu-module';
66
import {CdkMenuItemRadio} from './menu-item-radio';
77
import {CDK_MENU} from './menu-interface';
88
import {CdkMenu} from './menu';
9+
import {MENU_STACK, MenuStack} from '@angular/cdk-experimental/menu/menu-stack';
910

1011
describe('MenuItemRadio', () => {
1112
let fixture: ComponentFixture<SimpleRadioButton>;
@@ -21,6 +22,7 @@ describe('MenuItemRadio', () => {
2122
providers: [
2223
{provide: UniqueSelectionDispatcher, useValue: selectionDispatcher},
2324
{provide: CDK_MENU, useClass: CdkMenu},
25+
{provide: MENU_STACK, useClass: MenuStack},
2426
// View engine can't figure out the ElementRef to inject so we need to provide a fake
2527
{provide: ElementRef, useValue: new ElementRef<null>(null)},
2628
],

src/cdk-experimental/menu/menu-item-radio.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class CdkMenuItemRadio extends CdkMenuItemSelectable implements OnDestroy
4444
private readonly _selectionDispatcher: UniqueSelectionDispatcher,
4545
element: ElementRef<HTMLElement>,
4646
ngZone: NgZone,
47-
@Optional() @Inject(MENU_STACK) menuStack?: MenuStack,
47+
@Inject(MENU_STACK) menuStack: MenuStack,
4848
@Optional() @Inject(CDK_MENU) parentMenu?: Menu,
4949
@Optional() @Inject(MENU_AIM) menuAim?: MenuAim,
5050
@Optional() dir?: Directionality,

0 commit comments

Comments
 (0)