material-web
material-web copied to clipboard
Implement menu hoisting for select
Context
In select's shadow root, we use <mwc-menu>
this creates an <mwc-menu-surface>
which creates a surface that is position:absolute
. This gives the correct behavior of menu sticking and following around with the select.
Problem
The problem with this is that the stacking context of menu-surface is its current position in the tree. This means if there is an element after it in the tree with position: absolute
, then it will very likely cover the element. If the menu is placed inside of an element with position: absolute
or position: fixed
then the stacking context of the menu-surface is now placed inside that element.
Example / Repro
Workaround
In most cases the workaround is to add fixedMenuPosition
to mwc-select
. This will make the menu position: fixed
which creates a window-level stacking context thus positioning it above all position: absolute
s in the document.
Though this means if the user scrolls while the menu is open, then the menu will not "follow" the select on the page since fixed is relative to window not a relatively position element.
Proposed fix
The "Correct" fix for this is that mwc-menu
should be position:absolute
and placed as the last element in the document. This is what we call "hoisting the menu (to document.body)".
It does not make sense for the user to have to do this themselves and we don't want to do this within mwc-select itself as we don't want to step outside the lit WC rendering system as we do not know what renderers mwc-select will be placed in.
We should create a menu-hoisting / menu-requesting system. The real problem is how to handle templating, and css custom properties. e.g. something we napkined at the office
<body>
<nav>
<mwc-overlay-scope>
<nav-item>...</nav-item>
<nav-item>...</nav-item>
<nav-item>...</nav-item>
<nav-item>...</nav-item>
<nav-menu title="...">
<!-- without lit-html -->
<mwc-menu id="navMenu"></mwc-menu>
</mwc-overlay-scope>
</nav>
<mwc-overlay-scope>
<div id="application">
...
<!- with lit-html -->
<mwc-select @input=${...} label="fruits" .renderer=${(node: HTMLElement) => {
// import {html, render} from 'lit';
const template = html`
<mwc-list-item value="NONE" @click=${...}></mwc-list-item>
${items.map(item => html`
<mwc-list-item ?selected=${item.selected} value=${item.value}>
${item.text}
</mwc-list-item>
`)}
`;
}}>
</mwc-select
</div>
</mwc-overlay-scope>
<script type="jsx">
// or innerHTML or whatever rendering system you choose
import {render} from 'react-dom';
window.navMenu.renderer = async (node) => {
const template = (
<>
<ListItem>...</ListItem>
<ListItem>...</ListItem>
<ListItem>...</ListItem>
...
</>
);
render(template, node);
}
</script>
</body>
where the template of menu-overlay-scope
is:
<style>
:host {
display: contents;
}
</style>
<slot></slot>
<!-- some abstract surface that is used across tooltip and menus etc -->
<mwc-surface>
${callChildRenderer()}
</mwc-surface>
suggestion (as in #2409) align with native select behavior -
- menu is fixed
- opened menu inert the outside context
- user can't interact with the rest of the tree until menu is close
it is partially a modal like there's an invisible scrim underlying the menu
reasons -
-
aligns with native select
-
prevents adding - knowledge dependent - hoisting system for users to work against
-
bonus: aligns with angular material select
Think this is obsolete with M3 now