Skip to content

Commit f0d20ca

Browse files
crisbetokara
authored andcommitted
feat(menu): support typeahead focus (#7385)
1 parent 31e9775 commit f0d20ca

File tree

3 files changed

+51
-3
lines changed

3 files changed

+51
-3
lines changed

src/lib/menu/menu-directive.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export class MatMenu implements AfterContentInit, MatMenuPanel, OnDestroy {
150150
@Inject(MAT_MENU_DEFAULT_OPTIONS) private _defaultOptions: MatMenuDefaultOptions) { }
151151

152152
ngAfterContentInit() {
153-
this._keyManager = new FocusKeyManager<MatMenuItem>(this.items).withWrap();
153+
this._keyManager = new FocusKeyManager<MatMenuItem>(this.items).withWrap().withTypeAhead();
154154
this._tabSubscription = this._keyManager.tabOut.subscribe(() => this.close.emit('keydown'));
155155
}
156156

src/lib/menu/menu-item.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,5 +97,26 @@ export class MatMenuItem extends _MatMenuItemMixinBase implements FocusableOptio
9797
}
9898
}
9999

100+
/** Gets the label to be used when determining whether the option should be focused. */
101+
getLabel(): string {
102+
const element: HTMLElement = this._elementRef.nativeElement;
103+
let output = '';
104+
105+
if (element.childNodes) {
106+
const length = element.childNodes.length;
107+
108+
// Go through all the top-level text nodes and extract their text.
109+
// We skip anything that's not a text node to prevent the text from
110+
// being thrown off by something like an icon.
111+
for (let i = 0; i < length; i++) {
112+
if (element.childNodes[i].nodeType === Node.TEXT_NODE) {
113+
output += element.childNodes[i].textContent;
114+
}
115+
}
116+
}
117+
118+
return output.trim();
119+
}
120+
100121
}
101122

src/lib/menu/menu.spec.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
Output,
1010
TemplateRef,
1111
ViewChild,
12+
ViewChildren,
13+
QueryList,
1214
} from '@angular/core';
1315
import {Direction, Directionality} from '@angular/cdk/bidi';
1416
import {OverlayContainer} from '@angular/cdk/overlay';
@@ -21,6 +23,7 @@ import {
2123
MatMenuTrigger,
2224
MenuPositionX,
2325
MenuPositionY,
26+
MatMenuItem,
2427
} from './index';
2528
import {MENU_PANEL_TOP_PADDING} from './menu-trigger';
2629
import {extendObject} from '@angular/material/core';
@@ -49,7 +52,8 @@ describe('MatMenu', () => {
4952
CustomMenu,
5053
NestedMenu,
5154
NestedMenuCustomElevation,
52-
NestedMenuRepeater
55+
NestedMenuRepeater,
56+
FakeIcon
5357
],
5458
providers: [
5559
{provide: OverlayContainer, useFactory: () => {
@@ -175,6 +179,18 @@ describe('MatMenu', () => {
175179
expect(fixture.destroy.bind(fixture)).not.toThrow();
176180
});
177181

182+
it('should be able to extract the menu item text', () => {
183+
const fixture = TestBed.createComponent(SimpleMenu);
184+
fixture.detectChanges();
185+
expect(fixture.componentInstance.items.first.getLabel()).toBe('Item');
186+
});
187+
188+
it('should filter out non-text nodes when figuring out the label', () => {
189+
const fixture = TestBed.createComponent(SimpleMenu);
190+
fixture.detectChanges();
191+
expect(fixture.componentInstance.items.last.getLabel()).toBe('Item with an icon');
192+
});
193+
178194
describe('positions', () => {
179195
let fixture: ComponentFixture<PositionedMenu>;
180196
let panel: HTMLElement;
@@ -1070,7 +1086,7 @@ describe('MatMenu default overrides', () => {
10701086
beforeEach(async(() => {
10711087
TestBed.configureTestingModule({
10721088
imports: [MatMenuModule, NoopAnimationsModule],
1073-
declarations: [SimpleMenu],
1089+
declarations: [SimpleMenu, FakeIcon],
10741090
providers: [{
10751091
provide: MAT_MENU_DEFAULT_OPTIONS,
10761092
useValue: {overlapTrigger: false, xPosition: 'before', yPosition: 'above'},
@@ -1095,13 +1111,18 @@ describe('MatMenu default overrides', () => {
10951111
<mat-menu class="custom-one custom-two" #menu="matMenu" (close)="closeCallback($event)">
10961112
<button mat-menu-item> Item </button>
10971113
<button mat-menu-item disabled> Disabled </button>
1114+
<button mat-menu-item>
1115+
<fake-icon>unicorn</fake-icon>
1116+
Item with an icon
1117+
</button>
10981118
</mat-menu>
10991119
`
11001120
})
11011121
class SimpleMenu {
11021122
@ViewChild(MatMenuTrigger) trigger: MatMenuTrigger;
11031123
@ViewChild('triggerEl') triggerEl: ElementRef;
11041124
@ViewChild(MatMenu) menu: MatMenu;
1125+
@ViewChildren(MatMenuItem) items: QueryList<MatMenuItem>;
11051126
closeCallback = jasmine.createSpy('menu closed callback');
11061127
}
11071128

@@ -1284,3 +1305,9 @@ class NestedMenuRepeater {
12841305

12851306
items = ['one', 'two', 'three'];
12861307
}
1308+
1309+
@Component({
1310+
selector: 'fake-icon',
1311+
template: '<ng-content></ng-content>'
1312+
})
1313+
class FakeIcon { }

0 commit comments

Comments
 (0)