mirror of
https://github.com/material-components/material-web.git
synced 2024-10-26 21:56:56 +03:00
feat(menu)!: allow anchoring with idref string and set element ref on 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
This commit is contained in:
parent
3d59608571
commit
5ba348dfd0
@ -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';
|
||||
@ -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>
|
||||
@ -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
|
||||
@ -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>
|
||||
|
@ -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>
|
||||
@ -129,8 +125,7 @@ const linkable: MaterialStoryInit<StoryKnobs> = {
|
||||
};
|
||||
|
||||
const menu = renderMenu(
|
||||
knobs, secondAnchorRef, secondMenuRef,
|
||||
displayCloseEvent(secondOutputRef), false,
|
||||
knobs, secondMenuRef, displayCloseEvent(secondOutputRef), false,
|
||||
renderLinkableItems(fruitNames, knobs));
|
||||
|
||||
return html`
|
||||
@ -138,8 +133,7 @@ const linkable: MaterialStoryInit<StoryKnobs> = {
|
||||
<div style="position:relative;">
|
||||
|
||||
<md-filled-button
|
||||
@click=${showMenu}
|
||||
${ref(secondAnchorRef)}>
|
||||
@click=${showMenu} id="button">
|
||||
Open Menu
|
||||
</md-filled-button>
|
||||
${menu}
|
||||
@ -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}
|
||||
@ -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}
|
||||
@ -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}
|
||||
@ -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>();
|
||||
|
@ -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
|
||||
@ -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.
|
||||
*/
|
||||
@ -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,
|
||||
|
@ -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.
|
||||
|
@ -38,8 +38,7 @@ declare global {
|
||||
* ```html
|
||||
* <div style="position:relative;">
|
||||
* <button
|
||||
* class="anchor"
|
||||
* ${ref(anchorRef)}
|
||||
* id="anchor"
|
||||
* @click=${() => this.menuRef.value.show()}>
|
||||
* Click to open menu
|
||||
* </button>
|
||||
@ -47,8 +46,8 @@ declare global {
|
||||
* `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
|
||||
|
@ -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')!;
|
||||
|
@ -42,8 +42,7 @@ declare global {
|
||||
* ```html
|
||||
* <div style="position:relative;">
|
||||
* <button
|
||||
* class="anchor"
|
||||
* ${ref(anchorRef)}
|
||||
* id="anchor"
|
||||
* @click=${() => this.menuRef.value.show()}>
|
||||
* Click to open menu
|
||||
* </button>
|
||||
@ -51,8 +50,8 @@ declare global {
|
||||
* `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
|
||||
|
@ -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';
|
||||
@ -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[];
|
||||
@ -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"
|
||||
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user