Skip to content

Commit e3fad64

Browse files
committed
feat(cdk-experimental/menu): allow configuration of typeahead and menu position
Addresses several TODOs that were left after the original implementation
1 parent 356618d commit e3fad64

File tree

5 files changed

+44
-39
lines changed

5 files changed

+44
-39
lines changed

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

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,13 @@ export class ContextMenuTracker {
5757
/** Configuration options passed to the context menu. */
5858
export type ContextMenuOptions = {
5959
/** The opened menus X coordinate offset from the triggering position. */
60-
offsetX: number;
60+
offsetX?: number;
6161

6262
/** The opened menus Y coordinate offset from the triggering position. */
63-
offsetY: number;
63+
offsetY?: number;
64+
65+
/** Ordered list of preferred positions, from most to least desirable. */
66+
preferredPositions?: ConnectedPosition[];
6467
};
6568

6669
/** Injection token for the ContextMenu options object. */
@@ -83,9 +86,6 @@ export type ContextMenuCoordinates = {x: number; y: number};
8386
'(contextmenu)': '_openOnContextMenu($event)',
8487
},
8588
providers: [
86-
// In cases where the first menu item in the context menu is a trigger the submenu opens on a
87-
// hover event. Offsetting the opened context menu by 2px prevents this from occurring.
88-
{provide: CDK_CONTEXT_MENU_DEFAULT_OPTIONS, useValue: {offsetX: 2, offsetY: 2}},
8989
{provide: MENU_TRIGGER, useExisting: CdkContextMenuTrigger},
9090
{provide: MENU_STACK, useClass: MenuStack},
9191
],
@@ -129,7 +129,9 @@ export class CdkContextMenuTrigger extends MenuTrigger implements OnDestroy {
129129
private readonly _overlay: Overlay,
130130
private readonly _contextMenuTracker: ContextMenuTracker,
131131
@Inject(MENU_STACK) menuStack: MenuStack,
132-
@Inject(CDK_CONTEXT_MENU_DEFAULT_OPTIONS) private readonly _options: ContextMenuOptions,
132+
@Optional()
133+
@Inject(CDK_CONTEXT_MENU_DEFAULT_OPTIONS)
134+
private readonly _options?: ContextMenuOptions,
133135
@Optional() private readonly _directionality?: Directionality,
134136
) {
135137
super(injector, menuStack);
@@ -230,25 +232,21 @@ export class CdkContextMenuTrigger extends MenuTrigger implements OnDestroy {
230232
private _getOverlayPositionStrategy(
231233
coordinates: ContextMenuCoordinates,
232234
): FlexibleConnectedPositionStrategy {
235+
// In cases where the first menu item in the context menu is a trigger the submenu opens on a
236+
// hover event. We offset the context menu 2px by default to prevent this from occurring.
233237
return this._overlay
234238
.position()
235239
.flexibleConnectedTo(coordinates)
236-
.withDefaultOffsetX(this._options.offsetX)
237-
.withDefaultOffsetY(this._options.offsetY)
238-
.withPositions(this._getOverlayPositions());
239-
}
240-
241-
/**
242-
* Determine and return where to position the opened menu relative to the mouse location.
243-
*/
244-
private _getOverlayPositions(): ConnectedPosition[] {
245-
// TODO: this should be configurable through the injected context menu options
246-
return [
247-
{originX: 'end', originY: 'top', overlayX: 'start', overlayY: 'top'},
248-
{originX: 'start', originY: 'top', overlayX: 'end', overlayY: 'top'},
249-
{originX: 'end', originY: 'bottom', overlayX: 'start', overlayY: 'bottom'},
250-
{originX: 'start', originY: 'bottom', overlayX: 'end', overlayY: 'bottom'},
251-
];
240+
.withDefaultOffsetX(this._options?.offsetX ?? 2)
241+
.withDefaultOffsetY(this._options?.offsetY ?? 2)
242+
.withPositions(
243+
this._options?.preferredPositions ?? [
244+
{originX: 'end', originY: 'top', overlayX: 'start', overlayY: 'top'},
245+
{originX: 'start', originY: 'top', overlayX: 'end', overlayY: 'top'},
246+
{originX: 'end', originY: 'bottom', overlayX: 'start', overlayY: 'bottom'},
247+
{originX: 'start', originY: 'bottom', overlayX: 'end', overlayY: 'bottom'},
248+
],
249+
);
252250
}
253251

254252
/**

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,6 @@ class TriggersWithSameMenuSameMenuBar {
539539
@ViewChildren(CdkMenu) menus: QueryList<CdkMenu>;
540540
}
541541

542-
// TODO uncomment once we figure out why this is failing in Ivy
543542
@Component({
544543
template: `
545544
<div cdkMenuBar>

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,6 @@ export class CdkMenuItemTrigger extends MenuTrigger implements OnDestroy {
287287

288288
/** Determine and return where to position the opened menu relative to the menu item */
289289
private _getOverlayPositions(): ConnectedPosition[] {
290-
// TODO: use a common positioning config from (possibly) cdk/overlay
291290
return !this._parentMenu || this._parentMenu.orientation === 'horizontal'
292291
? [
293292
{originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top'},

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

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,6 @@ import {FocusNext, MENU_STACK, MenuStack} from './menu-stack';
3030
import {FocusableElement} from './pointer-focus-tracker';
3131
import {MENU_AIM, MenuAim, Toggler} from './menu-aim';
3232

33-
// TODO refactor this to be configurable allowing for custom elements to be removed
34-
/** Removes all icons from within the given element. */
35-
function removeIcons(element: Element) {
36-
for (const icon of Array.from(element.querySelectorAll('mat-icon, .material-icons'))) {
37-
icon.remove();
38-
}
39-
}
40-
4133
/**
4234
* Directive which provides the ability for an element to be focused and navigated to using the
4335
* keyboard when residing in a CdkMenu, CdkMenuBar, or CdkMenuGroup. It performs user defined
@@ -71,6 +63,12 @@ export class CdkMenuItem implements FocusableOption, FocusableElement, Toggler,
7163
}
7264
private _disabled = false;
7365

66+
/**
67+
* The text used to locate this item during menu typeahead. If not specified,
68+
* the `textContent` of the item will be used.
69+
*/
70+
@Input() typeahead: string;
71+
7472
/**
7573
* If this MenuItem is a regular MenuItem, outputs when it is triggered by a keyboard or mouse
7674
* event.
@@ -173,12 +171,7 @@ export class CdkMenuItem implements FocusableOption, FocusableElement, Toggler,
173171

174172
/** Get the label for this element which is required by the FocusableOption interface. */
175173
getLabel(): string {
176-
// TODO cloning the tree may be expensive; implement a better method
177-
// we know that the current node is an element type
178-
const clone = this._elementRef.nativeElement.cloneNode(true) as Element;
179-
removeIcons(clone);
180-
181-
return clone.textContent?.trim() || '';
174+
return this.typeahead || this._elementRef.nativeElement.textContent?.trim() || '';
182175
}
183176

184177
/**

src/material-experimental/menubar/menubar-item.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@
99
import {Component, ViewEncapsulation, ChangeDetectionStrategy} from '@angular/core';
1010
import {CdkMenuItem} from '@angular/cdk-experimental/menu';
1111

12+
/** Removes all icons from within the given element. */
13+
function removeIcons(element: Element) {
14+
for (const icon of Array.from(element.querySelectorAll('mat-icon, .material-icons'))) {
15+
icon.remove();
16+
}
17+
}
18+
1219
/**
1320
* A material design MenubarItem adhering to the functionality of CdkMenuItem and
1421
* CdkMenuItemTrigger. Its main purpose is to trigger menus and it lives inside of
@@ -30,4 +37,13 @@ import {CdkMenuItem} from '@angular/cdk-experimental/menu';
3037
},
3138
providers: [{provide: CdkMenuItem, useExisting: MatMenuBarItem}],
3239
})
33-
export class MatMenuBarItem extends CdkMenuItem {}
40+
export class MatMenuBarItem extends CdkMenuItem {
41+
override getLabel(): string {
42+
if (this.typeahead !== undefined) {
43+
return this.typeahead;
44+
}
45+
const clone = this._elementRef.nativeElement.cloneNode(true) as Element;
46+
removeIcons(clone);
47+
return clone.textContent?.trim() || '';
48+
}
49+
}

0 commit comments

Comments
 (0)