diff --git a/docs/components/figures/menu/usage-document.html b/docs/components/figures/menu/usage-document.html new file mode 100644 index 00000000000..77803b58ce9 --- /dev/null +++ b/docs/components/figures/menu/usage-document.html @@ -0,0 +1,30 @@ +
+
+
+
+ Open document menu +
+ + +
Apple
+
+ +
Banana
+
+ +
Cucumber
+
+
+
+ +
+
diff --git a/docs/components/figures/menu/usage-popover.html b/docs/components/figures/menu/usage-popover.html new file mode 100644 index 00000000000..6c0652e5eec --- /dev/null +++ b/docs/components/figures/menu/usage-popover.html @@ -0,0 +1,30 @@ +
+
+
+
+ Open popover menu +
+ + +
Apple
+
+ +
Banana
+
+ +
Cucumber
+
+
+
+ +
+
diff --git a/docs/components/images/menu/usage-document.webp b/docs/components/images/menu/usage-document.webp new file mode 100644 index 00000000000..8a50cf80403 Binary files /dev/null and b/docs/components/images/menu/usage-document.webp differ diff --git a/docs/components/images/menu/usage-popover.webp b/docs/components/images/menu/usage-popover.webp new file mode 100644 index 00000000000..0a86a82ecd6 Binary files /dev/null and b/docs/components/images/menu/usage-popover.webp differ diff --git a/docs/components/menu.md b/docs/components/menu.md index 44635dfc22c..fa104fde013 100644 --- a/docs/components/menu.md +++ b/docs/components/menu.md @@ -61,7 +61,7 @@ choices on a temporary surface. When opened, menus position themselves to an anchor. Thus, either `anchor` or `anchorElement` must be supplied to `md-menu` before opening. Additionally, a shared parent of `position:relative` should be present around the menu and it's -anchor. +anchor, because the default menu is positioned relative to the anchor element. Menus also render menu items such as `md-menu-item` and handle keyboard navigation between `md-menu-item`s as well as typeahead functionality. @@ -215,14 +215,69 @@ Granny Smith, and Red Delicious."](images/menu/usage-submenu.webp) ``` -### Fixed menus +### Popover-positioned menus Internally menu uses `position: absolute` by default. Though there are cases when the anchor and the node cannot share a common ancestor that is `position: relative`, or sometimes, menu will render below another item due to limitations -with `position: absolute`. In most of these cases, you would want to use the -`positioning="fixed"` attribute to position the menu relative to the window -instead of relative to the parent. +with `position: absolute`. + +Popover-positioned menus use the native +[Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) +to render above all other content. This may fix most issues where the default +menu positioning (`positioning="absolute"`) is not positioning as expected by +rendering into the +[top layer](google3/third_party/javascript/material/web/g3doc/docs/components/figures/menu/usage-fixed.html). + +> Warning: Popover API support was added in Chrome 114 and Safari 17. At the +> time of writing, Firefox does not support the Popover API +> ([see latest browser compatiblity](#fixed-positioned-menus)). +> +> For browsers that do not support the Popover API, `md-menu` will fall back to +> using [fixed-positioned menus](#fixed-positioned-menus). + + + +!["A filled button that says open popover menu. There is an open menu anchored +to the bottom of the button with three items, Apple, Banana, and +Cucumber."](images/menu/usage-popover.webp) + + + + +```html + +
+ Open popover menu +
+ + + + +
Apple
+
+ +
Banana
+
+ +
Cucumber
+
+
+ + +``` + +### Fixed-positioned menus + +This is the fallback implementation of +[popover-positioned menus](#popover-positioned-menus) and uses `position: fixed` +rather than the default `position: absolute` which calculates its position +relative to the window rather than the element. > Note: Fixed menu positions are positioned relative to the window and not the > document. This means that the menu will not scroll with the anchor as the page @@ -264,6 +319,64 @@ Cucumber."](images/menu/usage-fixed.webp) ``` +### Document-positioned menus + +When set to `positioning="document"`, `md-menu` will position itself relative to +the document as opposed to the element or the window from `"absolute"` and +`"fixed"` values respectively. + +Document level positioning is useful for the following cases: + +- There are no ancestor elements that produce a `relative` positioning + context. + - `position: relative` + - `position: absolute` + - `position: fixed` + - `transform: translate(x, y)` + - etc. +- The menu is hoisted to the top of the DOM + - The last child of `` + - [Top layer](https://developer.mozilla.org/en-US/docs/Glossary/Top_layer) + +- The same `md-menu` is being reused and the contents and anchors are being + dynamically changed + + + +!["A filled button that says open document menu. There is an open menu anchored +to the bottom of the button with three items, Apple, Banana, and +Cucumber."](images/menu/usage-document.webp) + + + + +```html + +
+ Open document menu +
+ + + + +
Apple
+
+ +
Banana
+
+ +
Cucumber
+
+
+ + +``` + ## Accessibility By default Menu is set up to function as a `role="menu"` with children as @@ -395,7 +508,6 @@ a sharp 0px border radius.](images/menu/theming.webp) ## API - ### MdMenu <md-menu> #### Properties diff --git a/menu/demo/demo.ts b/menu/demo/demo.ts index fb861ad6491..05b04a82560 100644 --- a/menu/demo/demo.ts +++ b/menu/demo/demo.ts @@ -64,10 +64,12 @@ const collection = new MaterialCollection>( }), new Knob('positioning', { defaultValue: 'absolute' as const, - ui: selectDropdown<'absolute' | 'fixed'>({ + ui: selectDropdown<'absolute' | 'fixed' | 'document' | 'popover'>({ options: [ {label: 'absolute', value: 'absolute'}, {label: 'fixed', value: 'fixed'}, + {label: 'document', value: 'document'}, + {label: 'popover', value: 'popover'}, ], }), }), diff --git a/menu/demo/stories.ts b/menu/demo/stories.ts index 7d2f7eb2e1a..fc48769745f 100644 --- a/menu/demo/stories.ts +++ b/menu/demo/stories.ts @@ -22,7 +22,7 @@ export interface StoryKnobs { anchorCorner: Corner | undefined; menuCorner: Corner | undefined; defaultFocus: FocusState | undefined; - positioning: 'absolute' | 'fixed' | undefined; + positioning: 'absolute' | 'fixed' | 'document' | 'popover' | undefined; open: boolean; quick: boolean; hasOverflow: boolean; @@ -98,7 +98,10 @@ const standard: MaterialStoryInit = { render(knobs) { return html`
-
+
= { return html`
-
+
= { return html`
-
+
= { ], render(knobs) { return html` -
+
This is the anchor (use the "open" knob)
` to render over everything or in a top-layer. + * - You are reusing a single `md-menu` element that dynamically renders + * content. + * + * __Examples for `position = 'popover'`:__ + * + * - Your browser supports `popover`. + * - Most cases. Once popover is in browsers, this will become the default. */ - @property() positioning: 'absolute' | 'fixed' = 'absolute'; + @property() positioning: 'absolute' | 'fixed' | 'document' | 'popover' = + 'absolute'; /** * Skips the opening and closing animations. */ @@ -229,6 +248,11 @@ export abstract class Menu extends LitElement { * The event path of the last window pointerdown event. */ private pointerPath: EventTarget[] = []; + + /** + * Whether or not the menu is repositoining due to window / document resize + */ + private isRepositioning = false; private readonly openCloseAnimationSignal = createAnimationSignal(); private readonly listController = new ListController({ @@ -347,7 +371,8 @@ export abstract class Menu extends LitElement { surfaceCorner: this.menuCorner, surfaceEl: this.surfaceEl, anchorEl: this.anchorElement, - positioning: this.positioning, + positioning: + this.positioning === 'popover' ? 'document' : this.positioning, isOpen: this.open, xOffset: this.xOffset, yOffset: this.yOffset, @@ -357,7 +382,10 @@ export abstract class Menu extends LitElement { // We can't resize components that have overflow like menus with // submenus because the overflow-y will show menu items / content // outside the bounds of the menu. (to be fixed w/ popover API) - repositionStrategy: this.hasOverflow ? 'move' : 'resize', + repositionStrategy: + this.hasOverflow && this.positioning !== 'popover' + ? 'move' + : 'resize', }; }, ); @@ -392,9 +420,33 @@ export abstract class Menu extends LitElement { } } + // Firefox does not support popover. Fall-back to using fixed. + if ( + changed.has('positioning') && + this.positioning === 'popover' && + // type required for Google JS conformance + !(this as unknown as {showPopover?: () => void}).showPopover + ) { + this.positioning = 'fixed'; + } + super.update(changed); } + private readonly onWindowResize = () => { + if ( + this.isRepositioning || + (this.positioning !== 'document' && + this.positioning !== 'fixed' && + this.positioning !== 'popover') + ) { + return; + } + this.isRepositioning = true; + this.reposition(); + this.isRepositioning = false; + }; + override connectedCallback() { super.connectedCallback(); if (this.open) { @@ -418,7 +470,8 @@ export abstract class Menu extends LitElement { return html`