mirror of
https://github.com/material-components/material-web.git
synced 2026-01-09 07:21:09 +08:00
test(menu): test all positioning values
PiperOrigin-RevId: 578986324
This commit is contained in:
parent
5bb4a422de
commit
afee41b5af
30
docs/components/figures/menu/usage-popover.html
Normal file
30
docs/components/figures/menu/usage-popover.html
Normal file
@ -0,0 +1,30 @@
|
||||
<div class="figure-wrapper">
|
||||
<figure
|
||||
style="justify-content: center"
|
||||
aria-label="A filled button that says open popover menu. Interact with the button to open a popover menu."
|
||||
>
|
||||
<div>
|
||||
<div style="margin: 16px">
|
||||
<md-filled-button id="usage-popover-anchor">Open popover menu</md-filled-button>
|
||||
</div>
|
||||
<md-menu positioning="popover" id="usage-popover" anchor="usage-popover-anchor">
|
||||
<md-menu-item>
|
||||
<div slot="headline">Apple</div>
|
||||
</md-menu-item>
|
||||
<md-menu-item>
|
||||
<div slot="headline">Banana</div>
|
||||
</md-menu-item>
|
||||
<md-menu-item>
|
||||
<div slot="headline">Cucumber</div>
|
||||
</md-menu-item>
|
||||
</md-menu>
|
||||
</div>
|
||||
<script type="module">
|
||||
const anchorEl = document.body.querySelector("#usage-popover-anchor");
|
||||
const menuEl = document.body.querySelector("#usage-popover");
|
||||
anchorEl.addEventListener("click", () => {
|
||||
menuEl.open = !menuEl.open;
|
||||
});
|
||||
</script>
|
||||
</figure>
|
||||
</div>
|
||||
BIN
docs/components/images/menu/usage-popover.webp
Normal file
BIN
docs/components/images/menu/usage-popover.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
@ -215,14 +215,69 @@ Granny Smith, and Red Delicious."](images/menu/usage-submenu.webp)
|
||||
</script>
|
||||
```
|
||||
|
||||
### 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)<!-- {.external} -->
|
||||
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)<!-- {.external} -->.
|
||||
|
||||
> 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)<!-- {.external} -->).
|
||||
>
|
||||
> For browsers that do not support the Popover API, `md-menu` will fall back to
|
||||
> using [fixed-positioned menus](#fixed-positioned-menus).
|
||||
|
||||
<!-- no-catalog-start -->
|
||||
|
||||

|
||||
|
||||
<!-- no-catalog-end -->
|
||||
<!-- catalog-include "figures/menu/usage-fixed.html" -->
|
||||
|
||||
```html
|
||||
<!-- Note the lack of position: relative parent. -->
|
||||
<div style="margin: 16px;">
|
||||
<md-filled-button id="usage-popover-anchor">Open popover menu</md-filled-button>
|
||||
</div>
|
||||
|
||||
<!-- popover menus do not require a common ancestor with the anchor. -->
|
||||
<md-menu positioning="popover" id="usage-popover" anchor="usage-popover-anchor">
|
||||
<md-menu-item>
|
||||
<div slot="headline">Apple</div>
|
||||
</md-menu-item>
|
||||
<md-menu-item>
|
||||
<div slot="headline">Banana</div>
|
||||
</md-menu-item>
|
||||
<md-menu-item>
|
||||
<div slot="headline">Cucumber</div>
|
||||
</md-menu-item>
|
||||
</md-menu>
|
||||
|
||||
<script type="module">
|
||||
const anchorEl = document.body.querySelector('#usage-popover-anchor');
|
||||
const menuEl = document.body.querySelector('#usage-popover');
|
||||
|
||||
anchorEl.addEventListener('click', () => { menuEl.open = !menuEl.open; });
|
||||
</script>
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
@ -64,11 +64,12 @@ const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>(
|
||||
}),
|
||||
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'},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 `<body>` 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`
|
||||
<div
|
||||
class="menu ${classMap(this.getSurfaceClasses())}"
|
||||
style=${styleMap(this.menuPositionController.surfaceStyles)}>
|
||||
style=${styleMap(this.menuPositionController.surfaceStyles)}
|
||||
popover=${this.positioning === 'popover' ? 'manual' : nothing}>
|
||||
${this.renderElevation()}
|
||||
<div class="items">
|
||||
<div class="item-padding"> ${this.renderMenuItems()} </div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user