material-web icon indicating copy to clipboard operation
material-web copied to clipboard

Implement menu hoisting for select

Open e111077 opened this issue 3 years ago • 1 comments

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

lit playground

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: absolutes 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>

e111077 avatar Jul 27 '21 01:07 e111077

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 -

yinonov avatar Aug 06 '21 05:08 yinonov

Think this is obsolete with M3 now

asyncLiz avatar Aug 02 '23 02:08 asyncLiz