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:

But it's easy to get stuff wrong with these, you can run into bugs like:
Layer inside layer getting clipped:

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

Layers getting captured inside of other positioned elements:

Layers replacing each other when rendered:

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.
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.
@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
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} />
}
Most helpful comment
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.