Skip to content

fix(material/menu): getLabel not working if text is inside indirect descendant node #20705

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions src/material-experimental/mdc-menu/menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -628,10 +628,18 @@ describe('MDC-based MatMenu', () => {
expect(fixture.componentInstance.items.first.getLabel()).toBe('Item');
});

it('should filter out non-text nodes when figuring out the label', () => {
it('should filter out icon nodes when figuring out the label', () => {
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
fixture.detectChanges();
expect(fixture.componentInstance.items.last.getLabel()).toBe('Item with an icon');
const items = fixture.componentInstance.items.toArray();
expect(items[2].getLabel()).toBe('Item with an icon');
});

it('should get the label of an item if the text is not in a direct descendant node', () => {
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
fixture.detectChanges();
const items = fixture.componentInstance.items.toArray();
expect(items[3].getLabel()).toBe('Item with text inside span');
});

it('should set the proper focus origin when opening by mouse', fakeAsync(() => {
Expand Down Expand Up @@ -2297,9 +2305,12 @@ describe('MatMenu default overrides', () => {
<button mat-menu-item> Item </button>
<button mat-menu-item disabled> Disabled </button>
<button mat-menu-item disableRipple>
<fake-icon>unicorn</fake-icon>
<mat-icon>unicorn</mat-icon>
Item with an icon
</button>
<button mat-menu-item>
<span>Item with text inside span</span>
</button>
<button *ngFor="let item of extraItems" mat-menu-item> {{item}} </button>
</mat-menu>
`
Expand Down Expand Up @@ -2520,7 +2531,7 @@ class SubmenuDeclaredInsideParentMenu {


@Component({
selector: 'fake-icon',
selector: 'mat-icon',
template: '<ng-content></ng-content>'
})
class FakeIcon {}
Expand Down
34 changes: 13 additions & 21 deletions src/material/menu/menu-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,6 @@ export class MatMenuItem extends _MatMenuItemMixinBase
/** ARIA role for the menu item. */
@Input() role: 'menuitem' | 'menuitemradio' | 'menuitemcheckbox' = 'menuitem';

private _document: Document;

/** Stream that emits when the menu item is hovered. */
readonly _hovered: Subject<MatMenuItem> = new Subject<MatMenuItem>();

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

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

Expand All @@ -89,8 +91,6 @@ export class MatMenuItem extends _MatMenuItemMixinBase
if (_parentMenu && _parentMenu.addItem) {
_parentMenu.addItem(this);
}

this._document = document;
}

/** Focuses the menu item. */
Expand Down Expand Up @@ -163,24 +163,16 @@ export class MatMenuItem extends _MatMenuItemMixinBase

/** Gets the label to be used when determining whether the option should be focused. */
getLabel(): string {
const element: HTMLElement = this._elementRef.nativeElement;
const textNodeType = this._document ? this._document.TEXT_NODE : 3;
let output = '';

if (element.childNodes) {
const length = element.childNodes.length;

// Go through all the top-level text nodes and extract their text.
// We skip anything that's not a text node to prevent the text from
// being thrown off by something like an icon.
for (let i = 0; i < length; i++) {
if (element.childNodes[i].nodeType === textNodeType) {
output += element.childNodes[i].textContent;
}
}
const clone = this._elementRef.nativeElement.cloneNode(true) as HTMLElement;
const icons = clone.querySelectorAll('mat-icon, .material-icons');

// Strip away icons so they don't show up in the text.
for (let i = 0; i < icons.length; i++) {
const icon = icons[i];
icon.parentNode?.removeChild(icon);
}

return output.trim();
return clone.textContent?.trim() || '';
}

static ngAcceptInputType_disabled: BooleanInput;
Expand Down
19 changes: 15 additions & 4 deletions src/material/menu/menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -626,10 +626,18 @@ describe('MatMenu', () => {
expect(fixture.componentInstance.items.first.getLabel()).toBe('Item');
});

it('should filter out non-text nodes when figuring out the label', () => {
it('should filter out icon nodes when figuring out the label', () => {
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
fixture.detectChanges();
expect(fixture.componentInstance.items.last.getLabel()).toBe('Item with an icon');
const items = fixture.componentInstance.items.toArray();
expect(items[2].getLabel()).toBe('Item with an icon');
});

it('should get the label of an item if the text is not in a direct descendant node', () => {
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
fixture.detectChanges();
const items = fixture.componentInstance.items.toArray();
expect(items[3].getLabel()).toBe('Item with text inside span');
});

it('should set the proper focus origin when opening by mouse', fakeAsync(() => {
Expand Down Expand Up @@ -2263,9 +2271,12 @@ describe('MatMenu default overrides', () => {
<button mat-menu-item> Item </button>
<button mat-menu-item disabled> Disabled </button>
<button mat-menu-item disableRipple>
<fake-icon>unicorn</fake-icon>
<mat-icon>unicorn</mat-icon>
Item with an icon
</button>
<button mat-menu-item>
<span>Item with text inside span</span>
</button>
<button *ngFor="let item of extraItems" mat-menu-item> {{item}} </button>
</mat-menu>
`
Expand Down Expand Up @@ -2486,7 +2497,7 @@ class SubmenuDeclaredInsideParentMenu {


@Component({
selector: 'fake-icon',
selector: 'mat-icon',
template: '<ng-content></ng-content>'
})
class FakeIcon {}
Expand Down
3 changes: 2 additions & 1 deletion tools/public_api_guard/material/menu.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ export declare class MatMenuItem extends _MatMenuItemMixinBase implements Focusa
_parentMenu?: MatMenuPanel<MatMenuItem> | undefined;
_triggersSubmenu: boolean;
role: 'menuitem' | 'menuitemradio' | 'menuitemcheckbox';
constructor(_elementRef: ElementRef<HTMLElement>, document?: any, _focusMonitor?: FocusMonitor | undefined, _parentMenu?: MatMenuPanel<MatMenuItem> | undefined);
constructor(_elementRef: ElementRef<HTMLElement>,
_document?: any, _focusMonitor?: FocusMonitor | undefined, _parentMenu?: MatMenuPanel<MatMenuItem> | undefined);
_checkDisabled(event: Event): void;
_getHostElement(): HTMLElement;
_getTabIndex(): string;
Expand Down