Reach-ui: Layered components

Created on 18 Sep 2018  路  8Comments  路  Source: reach/reach-ui

Certain components like menus or dialogs have elements which need to be positioned via "portals" in order to be positioned properly.

Here's an example of what a screen is _supposed_ to look like:

screen shot 2018-09-17 at 2 55 17 pm

But it's easy to get stuff wrong with these, you can run into bugs like:

Layer inside layer getting clipped:

screen shot 2018-09-17 at 2 56 22 pm

Layers "jumping" on top of layers that they shouldn't:

screen shot 2018-09-17 at 2 55 28 pm

Layers getting captured inside of other positioned elements:

screen shot 2018-09-17 at 3 02 08 pm

Layers replacing each other when rendered:

screen shot 2018-09-17 at 3 06 08 pm

I've long thought that React needed a good generic component that just handled this.

There's also issues of accessibility. But according to ARIA's authoring practices, the components that would use layers have very different kinds of keyboard interactions. For this reason, a11y should be implemented at the actual "widget" level.

I thought a generic component that solves this would be a useful addition to Reach.

Abstract API

function Menu() {
  return (
    <Layer offset={1}>
      ...
    </Layer>
  )
}

function Dialog() {
  return (
    <Layer offset={2}>
      ...
    </Layer>
  )
}

function App() {
  return (
    <main>
      <Menu/>
      <Dialog>
        <Menu/>
      </Dialog>
    </main>
  );
}

Which would get vaguely rendered like this (had to use web components syntax so GitHub wouldn't render syntax errors)

<main>
  ...
</main>
<x-portal>
  <div style="z-index: 1"><x-menu/></div>
  <div style="z-index: 2"><x-dialog/></div>
  <div style="z-index: 3"><x-menu/></div>
</x-portal>

What is essentially happening here is:

const { Provider, Consumer } = React.createContext(0);

function Layer(props) {
  return (
    <Consumer>
      {value => (
        <Provider value={value + props.offset}>
          {props.children(value + props.offset)}
        </Provider>
      )}
    </Consumer>
  );
}

And a ReactDOM.createPortal() mixed in there somewhere.

Stale Enhancement

Most helpful comment

How does outputting: ... differ?

Because the order of which elements get inserted in the portal is dependent on a depth-first-ish traversal of the DOM by React and in more complex interfaces that's not always reliable.

All 8 comments

@jamiebuilds I made something like that https://github.com/giuseppeg/react-layers-manager and extra a11y is added case by case eg: https://twitter.com/giuseppegurgone/status/1041743466647232513

Awesome write up @jamiebuilds. I've been relying on document order of the portals for this, and as long as you don't do weird crap w/ z-index, I'm hoping this is all we need.

Relying on document order alone is definitely not enough. I've seen plenty of bugs with that in my day /old man voice

How does outputting:

<x-portal>
  <div style="z-index: 1"><x-menu/></div>
  <div style="z-index: 2"><x-dialog/></div>
  <div style="z-index: 3"><x-menu/></div>
</x-portal>

differ?

I'm just trying to think of scenarios where people could goof up the layering and its the same either way, they have a bigger z-index on a root element on the document body, and I don't see how grouping those divs in x-portal is any safer.

they have a bigger z-index on a root element on the document body

@ryanflorence in my library I wrap the app to trap it in a stacking context

https://github.com/giuseppeg/react-layers-manager/blob/eb43d7a5efa04bf74ef19d9311b82e1950f2f6f6/src/LayersManager.js#L13-L16

Unfortunately this doesn't prevent libraries like Reach to append to the body and mess with my solution. IMHO layering should be done by the user or maybe be configurable. I imagine that you could use a declarative way to render with portals that can be easily replaced.

How does outputting: ... differ?

Because the order of which elements get inserted in the portal is dependent on a depth-first-ish traversal of the DOM by React and in more complex interfaces that's not always reliable.

@jamiebuilds is this an issue if one appends the "host" by hand and then renders inside of it with createPortal?

createPortal(a, container)
createPortal(b, container)
createPortal(c, container)

vs

createPortal(a, container1)
createPortal(b, container2)
createPortal(c, container3)

where containerN are nodes created by hand and manually appended to x-portal:

const container1 = xPortal.appendChild(document.createElement('div'))
createPortal(a, container1)

@jamiebuilds Do you think this possibly be solved by allowing an as prop on components that render a Portal so that users could bail out if needed? I'd need to explore this further but I'm not convinced any sort of stacking prop is the right solution. To your point, it may be better to let apps bail out and manage this if needed.

function MyOverlay(props) {
  return <DialogOverlay as={Layer} {...props} />
}
Was this page helpful?
0 / 5 - 0 ratings