Skip to content

Commit

Permalink
feat(menu)!: allow anchoring with idref string and set element ref on…
Browse files Browse the repository at this point in the history
… anchorElement

BREAKING: `MdMenu.prototype.anchor` now only accepts a string which will querySelector the rootNode of the menu. The method now to anchor to an element reference is to set `MdMenu.prototype.anchorElement`. This matches the `popover` anchoring proposal more closely, but that proposal may not pass in favor of a CSS approach.
PiperOrigin-RevId: 560955779
  • Loading branch information
e111077 authored and Copybara-Service committed Aug 29, 2023
1 parent 3d59608 commit 5ba348d
Show file tree
Hide file tree
Showing 8 changed files with 52 additions and 48 deletions.
8 changes: 3 additions & 5 deletions catalog/src/components/top-app-bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import '@material/web/icon/icon.js';

import type {MdIconButton} from '@material/web/iconbutton/icon-button.js';
import {css, html, LitElement} from 'lit';
import {customElement, query, state} from 'lit/decorators.js';
import {customElement, state} from 'lit/decorators.js';
import {live} from 'lit/directives/live.js';

import {drawerOpenSignal} from '../signals/drawer-open-state.js';
Expand All @@ -28,8 +28,6 @@ import {materialDesign} from '../svg/material-design-logo.js';
*/
@state() private menuOpen = false;

@query('.end md-icon-button') private paletteButton!: MdIconButton;

render() {
return html`
<header>
Expand Down Expand Up @@ -61,7 +59,7 @@ import {materialDesign} from '../svg/material-design-logo.js';
id="menu-island"
>
<md-menu
.anchor=${this.paletteButton}
anchor="theme-button"
menu-corner="START_END"
anchor-corner="END_END"
stay-open-on-focusout
Expand All @@ -70,7 +68,7 @@ import {materialDesign} from '../svg/material-design-logo.js';
>
<theme-changer></theme-changer>
</md-menu>
<md-icon-button @click="${this.onPaletteClick}">
<md-icon-button id="theme-button" @click="${this.onPaletteClick}">
<md-icon>palette</md-icon>
</md-icon-button>
</lit-island>
Expand Down
37 changes: 11 additions & 26 deletions menu/demo/stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,20 +99,16 @@ const standard: MaterialStoryInit<StoryKnobs> = {
};

const menu = renderMenu(
knobs, firstAnchorRef, firstMenuRef, displayCloseEvent(firstOutputRef),
false, renderItems(fruitNames, knobs));
knobs, firstMenuRef, displayCloseEvent(firstOutputRef), false,
renderItems(fruitNames, knobs));

return html`
<div class="root">
<div style="position:relative;">
<md-filled-button
@click=${showMenu}
${ref(firstAnchorRef)}>
<md-filled-button @click=${showMenu} id="button">
Open Menu
</md-filled-button>
${menu}
</div>
<div class="output" ${ref(firstOutputRef)}></div>
</div>
Expand All @@ -129,17 +125,15 @@ const linkable: MaterialStoryInit<StoryKnobs> = {
};

const menu = renderMenu(
knobs, secondAnchorRef, secondMenuRef,
displayCloseEvent(secondOutputRef), false,
knobs, secondMenuRef, displayCloseEvent(secondOutputRef), false,
renderLinkableItems(fruitNames, knobs));

return html`
<div class="root">
<div style="position:relative;">
<md-filled-button
@click=${showMenu}
${ref(secondAnchorRef)}>
@click=${showMenu} id="button">
Open Menu
</md-filled-button>
${menu}
Expand Down Expand Up @@ -167,16 +161,13 @@ const submenu: MaterialStoryInit<StoryKnobs> = {
};

const rootMenu = renderMenu(
knobs, thirdAnchorRef, thirdMenuRef, displayCloseEvent(thirdOutputRef),
true, layer0);
knobs, thirdMenuRef, displayCloseEvent(thirdOutputRef), true, layer0);

return html`
<div class="root">
<div style="position:relative;">
<md-filled-button
@click=${showMenu}
${ref(thirdAnchorRef)}>
<md-filled-button @click=${showMenu} id="button">
Open Menu
</md-filled-button>
${rootMenu}
Expand Down Expand Up @@ -208,15 +199,15 @@ const menuWithoutButton: MaterialStoryInit<StoryKnobs> = {
render(knobs) {
return html`
<div class="root" style="position:relative;">
<div id="anchor" ${ref(fourthAnchorRef)}>
<div id="anchor">
This is the anchor (use the "open" knob)
</div>
<md-menu slot="menu"
anchor="anchor"
.open=${knobs.open}
.quick=${knobs.quick}
.hasOverflow=${knobs.hasOverflow}
.ariaLabel=${knobs.ariaLabel}
.anchor=${fourthAnchorRef.value || null}
.anchorCorner="${knobs.anchorCorner!}"
.menuCorner="${knobs.menuCorner!}"
.xOffset=${knobs.xOffset}
Expand Down Expand Up @@ -308,15 +299,13 @@ function renderSubMenu(
}

function renderMenu(
knobs: StoryKnobs, anchorRef: Ref<HTMLElement>, menuRef: Ref<MdMenu>,
knobs: StoryKnobs, menuRef: Ref<MdMenu>,
onClose: (event: CloseMenuEvent) => void, hasOverflow: boolean,
...content: unknown[]) {
return html`
<md-menu
${ref(menuRef)}
${ref(() => {
menuRef.value!.anchor = anchorRef.value || null;
})}
anchor="button"
.quick=${knobs.quick}
.hasOverflow=${hasOverflow ?? knobs.hasOverflow}
.ariaLabel=${knobs.ariaLabel}
Expand All @@ -335,13 +324,9 @@ function renderMenu(
</md-menu>`;
}

const firstAnchorRef = createRef<HTMLElement>();
const firstMenuRef = createRef<MdMenu>();
const secondAnchorRef = createRef<HTMLElement>();
const secondMenuRef = createRef<MdMenu>();
const thirdAnchorRef = createRef<HTMLElement>();
const thirdMenuRef = createRef<MdMenu>();
const fourthAnchorRef = createRef<HTMLElement>();
const firstOutputRef = createRef<HTMLElement>();
const secondOutputRef = createRef<HTMLElement>();
const thirdOutputRef = createRef<HTMLElement>();
Expand Down
32 changes: 28 additions & 4 deletions menu/internal/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,13 @@ export abstract class Menu extends LitElement {
@query('slot') private readonly slotEl!: HTMLSlotElement|null;

/**
* The element in which the menu should align to.
* The ID of the element in the same root node in which the menu should align
* to. Overrides setting `anchorElement = elementReference`.
*
* __NOTE__: anchor or anchorElement must either be an HTMLElement or resolve
* to an HTMLElement in order for menu to open.
*/
@property({attribute: false})
anchor: HTMLElement&Partial<SurfacePositionTarget>|null = null;
@property() anchor = '';
/**
* Makes the element use `position:fixed` instead of `position:absolute`. In
* most cases, the menu should position itself above most other
Expand Down Expand Up @@ -200,6 +203,27 @@ export abstract class Menu extends LitElement {
};
});

private currentAnchorElement: HTMLElement|null = null;

/**
* The element which the menu should align to. If `anchor` is set to a
* non-empty idref string, then `anchorEl` will resolve to the element with
* the given id in the same root node. Otherwise, `null`.
*/
get anchorElement(): HTMLElement&Partial<SurfacePositionTarget>|null {
if (this.anchor) {
return (this.getRootNode() as Document | ShadowRoot)
.querySelector(`#${this.anchor}`);
}

return this.currentAnchorElement;
}

set anchorElement(element: HTMLElement&Partial<SurfacePositionTarget>|null) {
this.currentAnchorElement = element;
this.requestUpdate('anchorElement');
}

/**
* Handles positioning the surface and aligning it to the anchor.
*/
Expand All @@ -209,7 +233,7 @@ export abstract class Menu extends LitElement {
anchorCorner: this.anchorCorner,
surfaceCorner: this.menuCorner,
surfaceEl: this.surfaceEl,
anchorEl: this.anchor,
anchorEl: this.anchorElement,
isTopLayer: this.fixed,
isOpen: this.open,
xOffset: this.xOffset,
Expand Down
2 changes: 1 addition & 1 deletion menu/internal/submenuitem/sub-menu-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ export class SubMenuItem extends MenuItemEl {
menu.hasOverflow = true;
menu.anchorCorner = this.anchorCorner;
menu.menuCorner = this.menuCorner;
menu.anchor = this;
menu.anchorElement = this;
// We manually set focus with `active` on keyboard navigation. And we
// want to focus the root on hover, so the user can pick up navigation with
// keyboard after hover.
Expand Down
7 changes: 3 additions & 4 deletions menu/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,16 @@ declare global {
* ```html
* <div style="position:relative;">
* <button
* class="anchor"
* ${ref(anchorRef)}
* id="anchor"
* @click=${() => this.menuRef.value.show()}>
* Click to open menu
* </button>
* <!--
* `has-overflow` is required when using a submenu which overflows the
* menu's contents
* -->
* <md-menu has-overflow ${ref(menuRef)} ${(el) => el.anchor =
* anchorRef.value}> <md-menu-item header="This is a header"></md-menu-item>
* <md-menu anchor="anchor" has-overflow ${ref(menuRef)}>
* <md-menu-item header="This is a header"></md-menu-item>
* <md-sub-menu-item header="this is a submenu item">
* <md-menu slot="submenu">
* <md-menu-item
Expand Down
2 changes: 1 addition & 1 deletion menu/menu_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('<md-menu>', () => {

const button = root.querySelector('button')!;
const menu = root.querySelector('md-menu')!;
menu.anchor = button;
menu.anchorElement = button;
menu.show();
await menu.updateComplete;
const listEl = menu.renderRoot.querySelector('md-list')!;
Expand Down
7 changes: 3 additions & 4 deletions menu/sub-menu-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,16 @@ declare global {
* ```html
* <div style="position:relative;">
* <button
* class="anchor"
* ${ref(anchorRef)}
* id="anchor"
* @click=${() => this.menuRef.value.show()}>
* Click to open menu
* </button>
* <!--
* `has-overflow` is required when using a submenu which overflows the
* menu's contents
* -->
* <md-menu has-overflow ${ref(menuRef)} ${(el) => el.anchor =
* anchorRef.value}> <md-menu-item header="This is a header"></md-menu-item>
* <md-menu anchor="anchor" has-overflow ${ref(menuRef)}>
* <md-menu-item header="This is a header"></md-menu-item>
* <md-sub-menu-item header="this is a submenu item">
* <md-menu slot="submenu">
* <md-menu-item
Expand Down
5 changes: 2 additions & 3 deletions select/internal/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {property, query, queryAssignedElements, state} from 'lit/decorators.js';
import {classMap} from 'lit/directives/class-map.js';
import {html as staticHtml, StaticValue} from 'lit/static-html.js';

import {Field} from '../../field/internal/field.js';
import {List} from '../../list/internal/list.js';
import {DEFAULT_TYPEAHEAD_BUFFER_TIME, Menu} from '../../menu/internal/menu.js';
import {CloseMenuEvent, isElementInSubtree, isSelectableKey} from '../../menu/internal/shared.js';
Expand Down Expand Up @@ -93,7 +92,6 @@ export abstract class Select extends LitElement {

@state() private focused = false;
@state() private open = false;
@query('.field') private readonly field!: Field|null;
@query('md-menu') private readonly menu!: Menu|null;
@queryAssignedElements({slot: 'leadingicon', flatten: true})
private readonly leadingIcons!: Element[];
Expand Down Expand Up @@ -196,6 +194,7 @@ export abstract class Select extends LitElement {
aria-haspopup="listbox"
role="combobox"
part="field"
id="field"
tabindex=${this.disabled ? '-1' : '0'}
aria-expanded=${this.open ? 'true' : 'false'}
class="field"
Expand Down Expand Up @@ -262,7 +261,7 @@ export abstract class Select extends LitElement {
stay-open-on-focusout
part="menu"
exportparts="focus-ring: menu-focus-ring"
.anchor=${this.field}
anchor="field"
.open=${this.open}
.quick=${this.quick}
.fixed=${this.menuFixed}
Expand Down

0 comments on commit 5ba348d

Please sign in to comment.