From afee41b5af74ded798cd88627dd6759d0bf7356c Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Thu, 2 Nov 2023 14:50:46 -0700 Subject: [PATCH] test(menu): test all positioning values PiperOrigin-RevId: 578986324 --- .../figures/menu/usage-popover.html | 30 +++++++++ .../components/images/menu/usage-popover.webp | Bin 0 -> 6336 bytes docs/components/menu.md | 63 ++++++++++++++++-- menu/demo/demo.ts | 3 +- menu/demo/stories.ts | 2 +- menu/internal/_menu.scss | 13 +++- .../controllers/surfacePositionController.ts | 15 +++++ menu/internal/menu.ts | 46 ++++++++++--- 8 files changed, 155 insertions(+), 17 deletions(-) create mode 100644 docs/components/figures/menu/usage-popover.html create mode 100644 docs/components/images/menu/usage-popover.webp diff --git a/docs/components/figures/menu/usage-popover.html b/docs/components/figures/menu/usage-popover.html new file mode 100644 index 0000000000..6c0652e5ee --- /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-popover.webp b/docs/components/images/menu/usage-popover.webp new file mode 100644 index 0000000000000000000000000000000000000000..0a86a82ecd6a81f411c6dd48a6989f517a3c47cb GIT binary patch literal 6336 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 X^XMu~!*Ba5%uK`zxbgR!;@|lnf^SbM literal 0 HcmV?d00001 diff --git a/docs/components/menu.md b/docs/components/menu.md index d835bc7834..fa104fde01 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 453a578d02..05b04a8256 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 e6ecfac021..fc48769745 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 f089f08177..53d328eacc 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 77b8941f74..5d402d61f7 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 572e3e2f42..183c6162a7 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`