diff --git a/docs/components/figures/menu/usage-document.html b/docs/components/figures/menu/usage-document.html new file mode 100644 index 0000000000..77803b58ce --- /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/images/menu/usage-document.webp b/docs/components/images/menu/usage-document.webp new file mode 100644 index 0000000000..8a50cf8040 Binary files /dev/null and b/docs/components/images/menu/usage-document.webp differ diff --git a/docs/components/menu.md b/docs/components/menu.md index 44635dfc22..d835bc7834 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,14 @@ Granny Smith, and Red Delicious."](images/menu/usage-submenu.webp) ``` -### Fixed menus +### Fixed-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. +instead of relative to 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 +264,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 +453,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 fb861ad649..453a578d02 100644 --- a/menu/demo/demo.ts +++ b/menu/demo/demo.ts @@ -64,10 +64,11 @@ const collection = new MaterialCollection>( }), new Knob('positioning', { defaultValue: 'absolute' as const, - ui: selectDropdown<'absolute' | 'fixed'>({ + ui: selectDropdown<'absolute' | 'fixed' | 'document'>({ options: [ {label: 'absolute', value: 'absolute'}, {label: 'fixed', value: 'fixed'}, + {label: 'document', value: 'document'}, ], }), }), diff --git a/menu/demo/stories.ts b/menu/demo/stories.ts index 7d2f7eb2e1..e6ecfac021 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' | 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. */ - @property() positioning: 'absolute' | 'fixed' = 'absolute'; + @property() positioning: 'absolute' | 'fixed' | 'document' = 'absolute'; /** * Skips the opening and closing animations. */ @@ -229,6 +239,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({ @@ -395,6 +410,18 @@ export abstract class Menu extends LitElement { super.update(changed); } + private readonly onWindowResize = () => { + if ( + this.isRepositioning || + (this.positioning !== 'document' && this.positioning !== 'fixed') + ) { + return; + } + this.isRepositioning = true; + this.reposition(); + this.isRepositioning = false; + }; + override connectedCallback() { super.connectedCallback(); if (this.open) { @@ -449,7 +476,7 @@ export abstract class Menu extends LitElement { return html``; } - private getSurfaceClasses() { + private getSurfaceClasses(): ClassInfo { return { open: this.open, fixed: this.positioning === 'fixed', @@ -825,6 +852,8 @@ export abstract class Menu extends LitElement { private setUpGlobalEventListeners() { document.addEventListener('click', this.onDocumentClick, {capture: true}); window.addEventListener('pointerdown', this.onWindowPointerdown); + document.addEventListener('resize', this.onWindowResize, {passive: true}); + window.addEventListener('resize', this.onWindowResize, {passive: true}); } private cleanUpGlobalEventListeners() { @@ -832,6 +861,8 @@ export abstract class Menu extends LitElement { capture: true, }); window.removeEventListener('pointerdown', this.onWindowPointerdown); + document.removeEventListener('resize', this.onWindowResize); + window.removeEventListener('resize', this.onWindowResize); } private readonly onWindowPointerdown = (event: PointerEvent) => { @@ -930,4 +961,16 @@ export abstract class Menu extends LitElement { activatePreviousItem() { return this.listController.activatePreviousItem() ?? null; } + + /** + * Repositions the menu if it is open. + * + * Useful for the case where document or window-positioned menus have their + * anchors moved while open. + */ + reposition() { + if (this.open) { + this.menuPositionController.position(); + } + } } diff --git a/menu/internal/submenu/sub-menu.ts b/menu/internal/submenu/sub-menu.ts index 6b818d5bc6..e32394aa54 100644 --- a/menu/internal/submenu/sub-menu.ts +++ b/menu/internal/submenu/sub-menu.ts @@ -136,6 +136,16 @@ export class SubMenu extends LitElement { }, {once: true}, ); + + // Parent menu is `position: absolute` – this creates a new CSS relative + // positioning context (similar to doing `position: relative`), so the + // submenu's `` would be + // wrong even if we change `md-sub-menu` from `position: relative` to + // `position: static` because the submenu it would still be positioning + // itself relative to the parent menu. + if (menu.positioning === 'document') { + menu.positioning = 'absolute'; + } menu.quick = true; // Submenus are in overflow when not fixed. Can remove once we have native // popup support diff --git a/testing/table/internal/_test-table.scss b/testing/table/internal/_test-table.scss index 9ad860ca87..55a3ecf8d5 100644 --- a/testing/table/internal/_test-table.scss +++ b/testing/table/internal/_test-table.scss @@ -54,7 +54,7 @@ $dark-theme: tokens.md-comp-test-table-values( .md3-test-table__cell { border: 1px solid; padding: 16px; - position: relative; + position: var(--_cell-position); &::before { background-color: var(--_cell-color); diff --git a/tokens/_md-comp-test-table.scss b/tokens/_md-comp-test-table.scss index 62d40b4470..8b3bf53583 100644 --- a/tokens/_md-comp-test-table.scss +++ b/tokens/_md-comp-test-table.scss @@ -19,6 +19,7 @@ $_default: ( @function values($deps: $_default) { @return ( 'cell-color': map.get($deps, 'md-sys-color', 'surface-container-lowest'), + 'cell-position': 'relative', 'cell-text-color': map.get($deps, 'md-sys-color', 'on-surface'), 'cell-text-font': map.get($deps, 'md-sys-typescale', 'body-medium-font'), 'cell-text-size': map.get($deps, 'md-sys-typescale', 'body-medium-size'),