From dd60c5453b204435a5d0d5f3e622d1b16775034f Mon Sep 17 00:00:00 2001 From: crisbeto Date: Thu, 28 Sep 2017 11:19:38 +0200 Subject: [PATCH 1/2] feat(menu): support typeahead focus Adds support for focusing menu items via the character keys. --- src/lib/menu/menu-directive.ts | 2 +- src/lib/menu/menu-item.ts | 21 +++++++++++++++++++++ src/lib/menu/menu.spec.ts | 29 ++++++++++++++++++++++++++++- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/lib/menu/menu-directive.ts b/src/lib/menu/menu-directive.ts index d599e4ac58e4..b5d1a301d973 100644 --- a/src/lib/menu/menu-directive.ts +++ b/src/lib/menu/menu-directive.ts @@ -150,7 +150,7 @@ export class MatMenu implements AfterContentInit, MatMenuPanel, OnDestroy { @Inject(MAT_MENU_DEFAULT_OPTIONS) private _defaultOptions: MatMenuDefaultOptions) { } ngAfterContentInit() { - this._keyManager = new FocusKeyManager(this.items).withWrap(); + this._keyManager = new FocusKeyManager(this.items).withWrap().withTypeAhead(); this._tabSubscription = this._keyManager.tabOut.subscribe(() => this.close.emit('keydown')); } diff --git a/src/lib/menu/menu-item.ts b/src/lib/menu/menu-item.ts index c08a7e39cf3a..143d09553d60 100644 --- a/src/lib/menu/menu-item.ts +++ b/src/lib/menu/menu-item.ts @@ -97,5 +97,26 @@ export class MatMenuItem extends _MatMenuItemMixinBase implements FocusableOptio } } + /** Gets the label to be used when determining whether the option should be focused. */ + getLabel(): string { + const element: HTMLElement = this._elementRef.nativeElement; + 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 === Node.TEXT_NODE) { + output += element.childNodes[i].textContent; + } + } + } + + return output.trim(); + } + } diff --git a/src/lib/menu/menu.spec.ts b/src/lib/menu/menu.spec.ts index 83b8cb8db4e7..613b668a8c6b 100644 --- a/src/lib/menu/menu.spec.ts +++ b/src/lib/menu/menu.spec.ts @@ -9,6 +9,8 @@ import { Output, TemplateRef, ViewChild, + ViewChildren, + QueryList, } from '@angular/core'; import {Direction, Directionality} from '@angular/cdk/bidi'; import {OverlayContainer} from '@angular/cdk/overlay'; @@ -21,6 +23,7 @@ import { MatMenuTrigger, MenuPositionX, MenuPositionY, + MatMenuItem, } from './index'; import {MENU_PANEL_TOP_PADDING} from './menu-trigger'; import {extendObject} from '@angular/material/core'; @@ -49,7 +52,8 @@ describe('MatMenu', () => { CustomMenu, NestedMenu, NestedMenuCustomElevation, - NestedMenuRepeater + NestedMenuRepeater, + FakeIcon ], providers: [ {provide: OverlayContainer, useFactory: () => { @@ -175,6 +179,18 @@ describe('MatMenu', () => { expect(fixture.destroy.bind(fixture)).not.toThrow(); }); + it('should be able to extract the menu item text', () => { + const fixture = TestBed.createComponent(SimpleMenu); + fixture.detectChanges(); + expect(fixture.componentInstance.items.first.getLabel()).toBe('Item'); + }); + + it('should filter out non-text nodes when figuring out the label', () => { + const fixture = TestBed.createComponent(SimpleMenu); + fixture.detectChanges(); + expect(fixture.componentInstance.items.last.getLabel()).toBe('Item with an icon'); + }); + describe('positions', () => { let fixture: ComponentFixture; let panel: HTMLElement; @@ -1095,6 +1111,10 @@ describe('MatMenu default overrides', () => { + ` }) @@ -1102,6 +1122,7 @@ class SimpleMenu { @ViewChild(MatMenuTrigger) trigger: MatMenuTrigger; @ViewChild('triggerEl') triggerEl: ElementRef; @ViewChild(MatMenu) menu: MatMenu; + @ViewChildren(MatMenuItem) items: QueryList; closeCallback = jasmine.createSpy('menu closed callback'); } @@ -1284,3 +1305,9 @@ class NestedMenuRepeater { items = ['one', 'two', 'three']; } + +@Component({ + selector: 'fake-icon', + template: '' +}) +class FakeIcon { } From 731f606d989fb6564f8424f93fd384f257320f13 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Thu, 28 Sep 2017 15:33:26 +0200 Subject: [PATCH 2/2] chore: fix test failure --- src/lib/menu/menu.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/menu/menu.spec.ts b/src/lib/menu/menu.spec.ts index 613b668a8c6b..912fd29371ec 100644 --- a/src/lib/menu/menu.spec.ts +++ b/src/lib/menu/menu.spec.ts @@ -1086,7 +1086,7 @@ describe('MatMenu default overrides', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [MatMenuModule, NoopAnimationsModule], - declarations: [SimpleMenu], + declarations: [SimpleMenu, FakeIcon], providers: [{ provide: MAT_MENU_DEFAULT_OPTIONS, useValue: {overlapTrigger: false, xPosition: 'before', yPosition: 'above'},