Skip to content

Revert "fix(menu): keyboard controls not respecting DOM order when it… #16502

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

Closed
wants to merge 1 commit into from
Closed
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
2 changes: 1 addition & 1 deletion src/material/menu/menu-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class MatMenuItem extends _MatMenuItemMixinBase
private _elementRef: ElementRef<HTMLElement>,
@Inject(DOCUMENT) document?: any,
private _focusMonitor?: FocusMonitor,
@Inject(MAT_MENU_PANEL) @Optional() public _parentMenu?: MatMenuPanel<MatMenuItem>) {
@Inject(MAT_MENU_PANEL) @Optional() private _parentMenu?: MatMenuPanel<MatMenuItem>) {

// @breaking-change 8.0.0 make `_focusMonitor` and `document` required params.
super();
Expand Down
10 changes: 0 additions & 10 deletions src/material/menu/menu-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,6 @@ export interface MatMenuPanel<T = any> {
lazyContent?: MatMenuContent;
backdropClass?: string;
hasBackdrop?: boolean;

/**
* @deprecated To be removed.
* @breaking-change 8.0.0
*/
addItem?: (item: T) => void;

/**
* @deprecated To be removed.
* @breaking-change 8.0.0
*/
removeItem?: (item: T) => void;
}
44 changes: 1 addition & 43 deletions src/material/menu/menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -865,33 +865,6 @@ describe('MatMenu', () => {

expect(item.textContent!.trim()).toBe('two');
}));

it('should respect the DOM order, rather than insertion order, when moving focus using ' +
'the arrow keys', fakeAsync(() => {
let fixture = createComponent(SimpleMenuWithRepeater);

fixture.detectChanges();
fixture.componentInstance.trigger.openMenu();
fixture.detectChanges();
tick(500);

let menuPanel = document.querySelector('.mat-menu-panel')!;
let items = menuPanel.querySelectorAll('.mat-menu-panel [mat-menu-item]');

expect(document.activeElement).toBe(items[0], 'Expected first item to be focused on open');

// Add a new item after the first one.
fixture.componentInstance.items.splice(1, 0, 'Calzone');
fixture.detectChanges();

items = menuPanel.querySelectorAll('.mat-menu-panel [mat-menu-item]');
dispatchKeyboardEvent(menuPanel, 'keydown', DOWN_ARROW);
fixture.detectChanges();
tick();

expect(document.activeElement).toBe(items[1], 'Expected second item to be focused');
flush();
}));
});

describe('positions', () => {
Expand Down Expand Up @@ -2325,6 +2298,7 @@ class LazyMenuWithContext {
}



@Component({
template: `
<button [matMenuTriggerFor]="one">Toggle menu</button>
Expand Down Expand Up @@ -2357,19 +2331,3 @@ class DynamicPanelMenu {
class MenuWithCheckboxItems {
@ViewChild(MatMenuTrigger, {static: false}) trigger: MatMenuTrigger;
}


@Component({
template: `
<button [matMenuTriggerFor]="menu">Toggle menu</button>
<mat-menu #menu="matMenu">
<button *ngFor="let item of items" mat-menu-item>{{item}}</button>
</mat-menu>
`
})
class SimpleMenuWithRepeater {
@ViewChild(MatMenuTrigger, {static: false}) trigger: MatMenuTrigger;
@ViewChild(MatMenu, {static: false}) menu: MatMenu;
items = ['Pizza', 'Pasta'];
}

78 changes: 37 additions & 41 deletions src/material/menu/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ export class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatMenuItem>
private _yPosition: MenuPositionY = this._defaultOptions.yPosition;
private _previousElevation: string;

/** All items inside the menu. Includes items nested inside another menu. */
@ContentChildren(MatMenuItem, {descendants: true}) _allItems: QueryList<MatMenuItem>;
/** Menu items inside the current menu. */
private _items: MatMenuItem[] = [];

/** Only the direct descendant menu items. */
private _directDescendantItems = new QueryList<MatMenuItem>();
/** Emits whenever the amount of menu items changes. */
private _itemChanges = new Subject<MatMenuItem[]>();

/** Subscription to tab events on the menu panel */
private _tabSubscription = Subscription.EMPTY;
Expand Down Expand Up @@ -242,41 +242,23 @@ export class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatMenuItem>
}

ngAfterContentInit() {
this._updateDirectDescendants();
this._keyManager = new FocusKeyManager(this._directDescendantItems).withWrap().withTypeAhead();
this._keyManager = new FocusKeyManager<MatMenuItem>(this._items).withWrap().withTypeAhead();
this._tabSubscription = this._keyManager.tabOut.subscribe(() => this.closed.emit('tab'));
}

ngOnDestroy() {
this._directDescendantItems.destroy();
this._tabSubscription.unsubscribe();
this.closed.complete();
}

/** Stream that emits whenever the hovered menu item changes. */
_hovered(): Observable<MatMenuItem> {
return this._directDescendantItems.changes.pipe(
startWith(this._directDescendantItems),
switchMap(items => merge<MatMenuItem>(...items.map((item: MatMenuItem) => item._hovered)))
return this._itemChanges.pipe(
startWith(this._items),
switchMap(items => merge(...items.map(item => item._hovered)))
);
}

/*
* Registers a menu item with the menu.
* @docs-private
* @deprecated No longer being used. To be removed.
* @breaking-change 9.0.0
*/
addItem(_item: MatMenuItem) {}

/**
* Removes an item from the menu.
* @docs-private
* @deprecated No longer being used. To be removed.
* @breaking-change 9.0.0
*/
removeItem(_item: MatMenuItem) {}

/** Handle a keyboard event from the menu, delegating to the appropriate action. */
_handleKeydown(event: KeyboardEvent) {
const keyCode = event.keyCode;
Expand Down Expand Up @@ -357,6 +339,35 @@ export class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatMenuItem>
}
}

/**
* Registers a menu item with the menu.
* @docs-private
*/
addItem(item: MatMenuItem) {
// We register the items through this method, rather than picking them up through
// `ContentChildren`, because we need the items to be picked up by their closest
// `mat-menu` ancestor. If we used `@ContentChildren(MatMenuItem, {descendants: true})`,
// all descendant items will bleed into the top-level menu in the case where the consumer
// has `mat-menu` instances nested inside each other.
if (this._items.indexOf(item) === -1) {
this._items.push(item);
this._itemChanges.next(this._items);
}
}

/**
* Removes an item from the menu.
* @docs-private
*/
removeItem(item: MatMenuItem) {
const index = this._items.indexOf(item);

if (this._items.indexOf(item) > -1) {
this._items.splice(index, 1);
this._itemChanges.next(this._items);
}
}

/**
* Adds classes to the menu panel based on its position. Can be used by
* consumers to add specific styling based on the position.
Expand Down Expand Up @@ -403,21 +414,6 @@ export class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatMenuItem>
event.element.scrollTop = 0;
}
}

/**
* Sets up a stream that will keep track of any newly-added menu items and will update the list
* of direct descendants. We collect the descendants this way, because `_allItems` can include
* items that are part of child menus, and using a custom way of registering items is unreliable
* when it comes to maintaining the item order.
*/
private _updateDirectDescendants() {
this._allItems.changes
.pipe(startWith(this._allItems))
.subscribe((items: QueryList<MatMenuItem>) => {
this._directDescendantItems.reset(items.filter(item => item._parentMenu === this));
this._directDescendantItems.notifyOnChanges();
});
}
}

/** @docs-private We show the "_MatMenu" class as "MatMenu" in the docs. */
Expand Down
6 changes: 2 additions & 4 deletions tools/public_api_guard/material/menu.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ export declare class _MatMenu extends MatMenu {
}

export declare class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnInit, OnDestroy {
_allItems: QueryList<MatMenuItem>;
_animationDone: Subject<AnimationEvent>;
_classList: {
[key: string]: boolean;
Expand Down Expand Up @@ -31,12 +30,12 @@ export declare class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatM
_onAnimationStart(event: AnimationEvent): void;
_resetAnimation(): void;
_startAnimation(): void;
addItem(_item: MatMenuItem): void;
addItem(item: MatMenuItem): void;
focusFirstItem(origin?: FocusOrigin): void;
ngAfterContentInit(): void;
ngOnDestroy(): void;
ngOnInit(): void;
removeItem(_item: MatMenuItem): void;
removeItem(item: MatMenuItem): void;
resetActiveItem(): void;
setElevation(depth: number): void;
setPositionClasses(posX?: MenuPositionX, posY?: MenuPositionY): void;
Expand Down Expand Up @@ -80,7 +79,6 @@ export interface MatMenuDefaultOptions {
export declare class MatMenuItem extends _MatMenuItemMixinBase implements FocusableOption, CanDisable, CanDisableRipple, OnDestroy {
_highlighted: boolean;
readonly _hovered: Subject<MatMenuItem>;
_parentMenu?: MatMenuPanel<MatMenuItem> | undefined;
_triggersSubmenu: boolean;
role: 'menuitem' | 'menuitemradio' | 'menuitemcheckbox';
constructor(_elementRef: ElementRef<HTMLElement>, document?: any, _focusMonitor?: FocusMonitor | undefined, _parentMenu?: MatMenuPanel<MatMenuItem> | undefined);
Expand Down