Skip to content

Commit ab91e7c

Browse files
andy9775nielsr98
andauthored
refactor(cdk-experimental/menu): add more robust implementation of getLabel for menu item (#19979)
When determining the text to use for a menu item for TypeAhead ensure that text in nested elements isn't skipped and icons are skipped. Co-authored-by: Niels Rasmussen <[email protected]> Co-authored-by: Niels Rasmussen <[email protected]>
1 parent 69661f9 commit ab91e7c

File tree

2 files changed

+98
-13
lines changed

2 files changed

+98
-13
lines changed

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

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Component} from '@angular/core';
1+
import {Component, Type} from '@angular/core';
22
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
33
import {By} from '@angular/platform-browser';
44
import {CdkMenuModule} from './menu-module';
@@ -61,27 +61,59 @@ describe('MenuItem', () => {
6161
});
6262

6363
describe('with complex inner elements', () => {
64-
let fixture: ComponentFixture<ComplexMenuItem>;
6564
let menuItem: CdkMenuItem;
6665

67-
beforeEach(async(() => {
66+
/**
67+
* Build a component for testing and render it.
68+
* @param componentClass the component to create
69+
*/
70+
function createComponent<T>(componentClass: Type<T>) {
71+
let fixture: ComponentFixture<T>;
72+
6873
TestBed.configureTestingModule({
6974
imports: [CdkMenuModule],
70-
declarations: [ComplexMenuItem],
75+
declarations: [componentClass, MatIcon],
7176
providers: [{provide: CDK_MENU, useClass: CdkMenu}],
7277
}).compileComponents();
73-
}));
7478

75-
beforeEach(() => {
76-
fixture = TestBed.createComponent(ComplexMenuItem);
79+
fixture = TestBed.createComponent(componentClass);
7780
fixture.detectChanges();
7881

7982
menuItem = fixture.debugElement.query(By.directive(CdkMenuItem)).injector.get(CdkMenuItem);
83+
}
84+
85+
it('should get the text for a simple menu item with no nested or wrapped elements', () => {
86+
createComponent(SingleMenuItem);
87+
expect(menuItem.getLabel()).toEqual('Click me!');
8088
});
8189

82-
it('should be able to extract the label text, with text nested in bold tag', () => {
83-
expect(menuItem.getLabel()).toBe('Click me!');
90+
it('should get the text for menu item with a single nested mat icon component', () => {
91+
createComponent(MenuItemWithIcon);
92+
expect(menuItem.getLabel()).toEqual('Click me!');
8493
});
94+
95+
it(
96+
'should get the text for menu item with single nested component with the material ' +
97+
'icon class',
98+
() => {
99+
createComponent(MenuItemWithIconClass);
100+
expect(menuItem.getLabel()).toEqual('Click me!');
101+
}
102+
);
103+
104+
it('should get the text for a menu item with bold marked text', () => {
105+
createComponent(MenuItemWithBoldElement);
106+
expect(menuItem.getLabel()).toEqual('Click me!');
107+
});
108+
109+
it(
110+
'should get the text for a menu item with nested icon, nested icon class and nested ' +
111+
'wrapping elements',
112+
() => {
113+
createComponent(MenuItemWithMultipleNestings);
114+
expect(menuItem.getLabel()).toEqual('Click me!');
115+
}
116+
);
85117
});
86118
});
87119

@@ -91,6 +123,47 @@ describe('MenuItem', () => {
91123
class SingleMenuItem {}
92124

93125
@Component({
94-
template: ` <button cdkMenuItem>Click <b>me!</b></button> `,
126+
template: `
127+
<button cdkMenuItem>
128+
<mat-icon>unicorn</mat-icon>
129+
Click me!
130+
</button>
131+
`,
132+
})
133+
class MenuItemWithIcon {}
134+
@Component({
135+
template: `
136+
<button cdkMenuItem>
137+
<div class="material-icons">unicorn</div>
138+
Click me!
139+
</button>
140+
`,
141+
})
142+
class MenuItemWithIconClass {}
143+
144+
@Component({
145+
template: ` <button cdkMenuItem><b>Click</b> me!</button> `,
146+
})
147+
class MenuItemWithBoldElement {}
148+
149+
@Component({
150+
template: `
151+
<button cdkMenuItem>
152+
<div>
153+
<div class="material-icons">unicorn</div>
154+
<div>
155+
Click
156+
</div>
157+
<mat-icon>menu</mat-icon>
158+
<div>me<b>!</b></div>
159+
</div>
160+
</button>
161+
`,
162+
})
163+
class MenuItemWithMultipleNestings {}
164+
165+
@Component({
166+
selector: 'mat-icon',
167+
template: '<ng-content></ng-content>',
95168
})
96-
class ComplexMenuItem {}
169+
class MatIcon {}

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ import {CdkMenuItemTrigger} from './menu-item-trigger';
2525
import {Menu, CDK_MENU} from './menu-interface';
2626
import {FocusNext} from './menu-stack';
2727

28+
// TODO refactor this to be configurable allowing for custom elements to be removed
29+
/** Removes all icons from within the given element. */
30+
function removeIcons(element: Element) {
31+
for (const icon of Array.from(element.querySelectorAll('mat-icon, .material-icons'))) {
32+
icon.parentNode?.removeChild(icon);
33+
}
34+
}
35+
2836
/**
2937
* Directive which provides the ability for an element to be focused and navigated to using the
3038
* keyboard when residing in a CdkMenu, CdkMenuBar, or CdkMenuGroup. It performs user defined
@@ -106,8 +114,12 @@ export class CdkMenuItem implements FocusableOption {
106114

107115
/** Get the label for this element which is required by the FocusableOption interface. */
108116
getLabel(): string {
109-
// TODO(andy): implement a more robust algorithm for determining nested text
110-
return this._elementRef.nativeElement.textContent || '';
117+
// TODO cloning the tree may be expensive; implement a better method
118+
// we know that the current node is an element type
119+
const clone = this._elementRef.nativeElement.cloneNode(true) as Element;
120+
removeIcons(clone);
121+
122+
return clone.textContent?.trim() || '';
111123
}
112124

113125
// In Ivy the `host` metadata will be merged, whereas in ViewEngine it is overridden. In order

0 commit comments

Comments
 (0)