Skip to content

Commit a461929

Browse files
authored
fix(material/menu): getLabel not working if text is inside indirect descendant node (#20705)
Currently `MatMenuItem.getLabel` only looks at direct descendant text nodes in order to figure out its text while excluding things like icons. This breaks down if there's some kind of wrapper in the menu item. These changes fix the issue by using `textContent` and excluding icons specifically. Fixes #20200.
1 parent 6d86f0f commit a461929

File tree

4 files changed

+45
-30
lines changed

4 files changed

+45
-30
lines changed

src/material-experimental/mdc-menu/menu.spec.ts

+15-4
Original file line numberDiff line numberDiff line change
@@ -628,10 +628,18 @@ describe('MDC-based MatMenu', () => {
628628
expect(fixture.componentInstance.items.first.getLabel()).toBe('Item');
629629
});
630630

631-
it('should filter out non-text nodes when figuring out the label', () => {
631+
it('should filter out icon nodes when figuring out the label', () => {
632632
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
633633
fixture.detectChanges();
634-
expect(fixture.componentInstance.items.last.getLabel()).toBe('Item with an icon');
634+
const items = fixture.componentInstance.items.toArray();
635+
expect(items[2].getLabel()).toBe('Item with an icon');
636+
});
637+
638+
it('should get the label of an item if the text is not in a direct descendant node', () => {
639+
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
640+
fixture.detectChanges();
641+
const items = fixture.componentInstance.items.toArray();
642+
expect(items[3].getLabel()).toBe('Item with text inside span');
635643
});
636644

637645
it('should set the proper focus origin when opening by mouse', fakeAsync(() => {
@@ -2297,9 +2305,12 @@ describe('MatMenu default overrides', () => {
22972305
<button mat-menu-item> Item </button>
22982306
<button mat-menu-item disabled> Disabled </button>
22992307
<button mat-menu-item disableRipple>
2300-
<fake-icon>unicorn</fake-icon>
2308+
<mat-icon>unicorn</mat-icon>
23012309
Item with an icon
23022310
</button>
2311+
<button mat-menu-item>
2312+
<span>Item with text inside span</span>
2313+
</button>
23032314
<button *ngFor="let item of extraItems" mat-menu-item> {{item}} </button>
23042315
</mat-menu>
23052316
`
@@ -2520,7 +2531,7 @@ class SubmenuDeclaredInsideParentMenu {
25202531

25212532

25222533
@Component({
2523-
selector: 'fake-icon',
2534+
selector: 'mat-icon',
25242535
template: '<ng-content></ng-content>'
25252536
})
25262537
class FakeIcon {}

src/material/menu/menu-item.ts

+13-21
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,6 @@ export class MatMenuItem extends _MatMenuItemMixinBase
6363
/** ARIA role for the menu item. */
6464
@Input() role: 'menuitem' | 'menuitemradio' | 'menuitemcheckbox' = 'menuitem';
6565

66-
private _document: Document;
67-
6866
/** Stream that emits when the menu item is hovered. */
6967
readonly _hovered: Subject<MatMenuItem> = new Subject<MatMenuItem>();
7068

@@ -79,7 +77,11 @@ export class MatMenuItem extends _MatMenuItemMixinBase
7977

8078
constructor(
8179
private _elementRef: ElementRef<HTMLElement>,
82-
@Inject(DOCUMENT) document?: any,
80+
/**
81+
* @deprecated `_document` parameter is no longer being used and will be removed.
82+
* @breaking-change 12.0.0
83+
*/
84+
@Inject(DOCUMENT) _document?: any,
8385
private _focusMonitor?: FocusMonitor,
8486
@Inject(MAT_MENU_PANEL) @Optional() public _parentMenu?: MatMenuPanel<MatMenuItem>) {
8587

@@ -89,8 +91,6 @@ export class MatMenuItem extends _MatMenuItemMixinBase
8991
if (_parentMenu && _parentMenu.addItem) {
9092
_parentMenu.addItem(this);
9193
}
92-
93-
this._document = document;
9494
}
9595

9696
/** Focuses the menu item. */
@@ -163,24 +163,16 @@ export class MatMenuItem extends _MatMenuItemMixinBase
163163

164164
/** Gets the label to be used when determining whether the option should be focused. */
165165
getLabel(): string {
166-
const element: HTMLElement = this._elementRef.nativeElement;
167-
const textNodeType = this._document ? this._document.TEXT_NODE : 3;
168-
let output = '';
169-
170-
if (element.childNodes) {
171-
const length = element.childNodes.length;
172-
173-
// Go through all the top-level text nodes and extract their text.
174-
// We skip anything that's not a text node to prevent the text from
175-
// being thrown off by something like an icon.
176-
for (let i = 0; i < length; i++) {
177-
if (element.childNodes[i].nodeType === textNodeType) {
178-
output += element.childNodes[i].textContent;
179-
}
180-
}
166+
const clone = this._elementRef.nativeElement.cloneNode(true) as HTMLElement;
167+
const icons = clone.querySelectorAll('mat-icon, .material-icons');
168+
169+
// Strip away icons so they don't show up in the text.
170+
for (let i = 0; i < icons.length; i++) {
171+
const icon = icons[i];
172+
icon.parentNode?.removeChild(icon);
181173
}
182174

183-
return output.trim();
175+
return clone.textContent?.trim() || '';
184176
}
185177

186178
static ngAcceptInputType_disabled: BooleanInput;

src/material/menu/menu.spec.ts

+15-4
Original file line numberDiff line numberDiff line change
@@ -626,10 +626,18 @@ describe('MatMenu', () => {
626626
expect(fixture.componentInstance.items.first.getLabel()).toBe('Item');
627627
});
628628

629-
it('should filter out non-text nodes when figuring out the label', () => {
629+
it('should filter out icon nodes when figuring out the label', () => {
630630
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
631631
fixture.detectChanges();
632-
expect(fixture.componentInstance.items.last.getLabel()).toBe('Item with an icon');
632+
const items = fixture.componentInstance.items.toArray();
633+
expect(items[2].getLabel()).toBe('Item with an icon');
634+
});
635+
636+
it('should get the label of an item if the text is not in a direct descendant node', () => {
637+
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
638+
fixture.detectChanges();
639+
const items = fixture.componentInstance.items.toArray();
640+
expect(items[3].getLabel()).toBe('Item with text inside span');
633641
});
634642

635643
it('should set the proper focus origin when opening by mouse', fakeAsync(() => {
@@ -2263,9 +2271,12 @@ describe('MatMenu default overrides', () => {
22632271
<button mat-menu-item> Item </button>
22642272
<button mat-menu-item disabled> Disabled </button>
22652273
<button mat-menu-item disableRipple>
2266-
<fake-icon>unicorn</fake-icon>
2274+
<mat-icon>unicorn</mat-icon>
22672275
Item with an icon
22682276
</button>
2277+
<button mat-menu-item>
2278+
<span>Item with text inside span</span>
2279+
</button>
22692280
<button *ngFor="let item of extraItems" mat-menu-item> {{item}} </button>
22702281
</mat-menu>
22712282
`
@@ -2486,7 +2497,7 @@ class SubmenuDeclaredInsideParentMenu {
24862497

24872498

24882499
@Component({
2489-
selector: 'fake-icon',
2500+
selector: 'mat-icon',
24902501
template: '<ng-content></ng-content>'
24912502
})
24922503
class FakeIcon {}

tools/public_api_guard/material/menu.d.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ export declare class MatMenuItem extends _MatMenuItemMixinBase implements Focusa
104104
_parentMenu?: MatMenuPanel<MatMenuItem> | undefined;
105105
_triggersSubmenu: boolean;
106106
role: 'menuitem' | 'menuitemradio' | 'menuitemcheckbox';
107-
constructor(_elementRef: ElementRef<HTMLElement>, document?: any, _focusMonitor?: FocusMonitor | undefined, _parentMenu?: MatMenuPanel<MatMenuItem> | undefined);
107+
constructor(_elementRef: ElementRef<HTMLElement>,
108+
_document?: any, _focusMonitor?: FocusMonitor | undefined, _parentMenu?: MatMenuPanel<MatMenuItem> | undefined);
108109
_checkDisabled(event: Event): void;
109110
_getHostElement(): HTMLElement;
110111
_getTabIndex(): string;

0 commit comments

Comments
 (0)