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 @@
+
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 d835bc78342..fa104fde013 100644
--- a/docs/components/menu.md
+++ b/docs/components/menu.md
@@ -215,14 +215,69 @@ Granny Smith, and Red Delicious."](images/menu/usage-submenu.webp)
```
-### Fixed-positioned 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 element.
+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
diff --git a/menu/demo/demo.ts b/menu/demo/demo.ts
index 453a578d02a..05b04a82560 100644
--- a/menu/demo/demo.ts
+++ b/menu/demo/demo.ts
@@ -64,11 +64,12 @@ const collection = new MaterialCollection>(
}),
new Knob('positioning', {
defaultValue: 'absolute' as const,
- ui: selectDropdown<'absolute' | 'fixed' | 'document'>({
+ 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 e6ecfac021d..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' | 'document' | undefined;
+ positioning: 'absolute' | 'fixed' | 'document' | 'popover' | undefined;
open: boolean;
quick: boolean;
hasOverflow: boolean;
diff --git a/menu/internal/_menu.scss b/menu/internal/_menu.scss
index f089f081774..53d328eacc3 100644
--- a/menu/internal/_menu.scss
+++ b/menu/internal/_menu.scss
@@ -60,6 +60,12 @@
.menu {
border-radius: map.get($tokens, 'container-shape');
display: none;
+ inset: auto;
+ border: none;
+ padding: 0px;
+ overflow: visible;
+ // [popover] adds a canvas background
+ background-color: transparent;
opacity: 0;
z-index: 20;
position: absolute;
@@ -70,6 +76,10 @@
max-width: inherit;
}
+ .menu::backdrop {
+ display: none;
+ }
+
.fixed {
position: fixed;
}
@@ -93,10 +103,11 @@
padding-block: 8px;
}
- .has-overflow .items {
+ .has-overflow:not([popover]) .items {
overflow: visible;
}
+ .has-overflow.animating .items,
.animating .items {
overflow: hidden;
}
diff --git a/menu/internal/controllers/surfacePositionController.ts b/menu/internal/controllers/surfacePositionController.ts
index 77b8941f746..5d402d61f7b 100644
--- a/menu/internal/controllers/surfacePositionController.ts
+++ b/menu/internal/controllers/surfacePositionController.ts
@@ -196,6 +196,14 @@ export class SurfacePositionController implements ReactiveController {
this.host.requestUpdate();
await this.host.updateComplete;
+ // Safari has a bug that makes popovers render incorrectly if the node is
+ // made visible + Animation Frame before calling showPopover().
+ // https://bugs.webkit.org/show_bug.cgi?id=264069
+ // also the cast is required due to differing TS types in Google and OSS.
+ if ((surfaceEl as unknown as {popover: string}).popover) {
+ (surfaceEl as unknown as {showPopover: () => void}).showPopover();
+ }
+
const surfaceRect = surfaceEl.getSurfacePositionClientRect
? surfaceEl.getSurfacePositionClientRect()
: surfaceEl.getBoundingClientRect();
@@ -600,5 +608,12 @@ export class SurfacePositionController implements ReactiveController {
'display': 'none',
};
this.host.requestUpdate();
+ const surfaceEl = this.getProperties().surfaceEl;
+
+ // The following type casts are required due to differing TS types in Google
+ // and open source.
+ if ((surfaceEl as unknown as {popover?: string})?.popover) {
+ (surfaceEl as unknown as {hidePopover: () => void}).hidePopover();
+ }
}
}
diff --git a/menu/internal/menu.ts b/menu/internal/menu.ts
index 572e3e2f42d..183c6162a7e 100644
--- a/menu/internal/menu.ts
+++ b/menu/internal/menu.ts
@@ -7,7 +7,7 @@
import '../../elevation/elevation.js';
import '../../focus/md-focus-ring.js';
-import {html, isServer, LitElement, PropertyValues} from 'lit';
+import {LitElement, PropertyValues, html, isServer, nothing} from 'lit';
import {property, query, queryAssignedElements, state} from 'lit/decorators.js';
import {ClassInfo, classMap} from 'lit/directives/class-map.js';
import {styleMap} from 'lit/directives/style-map.js';
@@ -16,7 +16,7 @@ import {
polyfillARIAMixin,
polyfillElementInternalsAria,
} from '../../internal/aria/aria.js';
-import {createAnimationSignal, EASING} from '../../internal/motion/animation.js';
+import {EASING, createAnimationSignal} from '../../internal/motion/animation.js';
import {
ListController,
NavigableKeys,
@@ -107,9 +107,12 @@ export abstract class Menu extends LitElement {
@property() anchor = '';
/**
* Whether the positioning algorithim should calculate relative to the parent
- * of the anchor element (absolute) or relative to the window (fixed).
+ * of the anchor element (`absolute`), relative to the window (`fixed`), or
+ * relative to the document (`document`). `popover` will use the popover API
+ * to render the menu in the top-layer. If your browser does not support the
+ * popover API, it will revert to `fixed`.
*
- * Examples for `position = 'fixed'`:
+ * __Examples for `position = 'fixed'`:__
*
* - If there is no `position:relative` in the given parent tree and the
* surface is `position:absolute`
@@ -118,7 +121,7 @@ export abstract class Menu extends LitElement {
* - The anchor and the surface do not share a common `position:relative`
* ancestor
*
- * When using positioning = fixed, in most cases, the menu should position
+ * When using `positioning=fixed`, in most cases, the menu should position
* itself above most other `position:absolute` or `position:fixed` elements
* when placed inside of them. e.g. using a menu inside of an `md-dialog`.
*
@@ -134,8 +137,14 @@ export abstract class Menu extends LitElement {
* end of the `` 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' | 'document' = 'absolute';
+ @property() positioning: 'absolute' | 'fixed' | 'document' | 'popover' =
+ 'absolute';
/**
* Skips the opening and closing animations.
*/
@@ -362,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,
@@ -372,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',
};
},
);
@@ -407,13 +420,25 @@ 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 !== 'document' &&
+ this.positioning !== 'fixed' &&
+ this.positioning !== 'popover')
) {
return;
}
@@ -445,7 +470,8 @@ export abstract class Menu extends LitElement {
return html`