Ok, before I plunge deep into this task I wanted to take the time to give a bit of history and highlights the different concerns at play here. Then I have a rough plan for what to try now.
cc @jjenzz and @chaance, have a read, hopefully that gives you a bit more context.
History
So back in the days, when developing the original primitives, the Lock
came about as the solution we needed to deal with everything a Dialog
needs to do to be accessible. By extension later this was also applied to Popover
and subsequently used by Menu
and all its usages (DropdownMenu
, ContextMenu
, Select
).
There might have already been some assumptions made at this level.
Most of the functionality was implemented as part of a lower level JS only createFocusTrap
utility. This was in an effort to make sure none of this critical behaviour is react-specific as we expect to implement these components in other view layers in the future.
Features in question
As an example here's a list of what needs to be done for a Dialog
to be accessible:
- just before opening:
- store previously focused element before opening
- when opening:
- make the content element itself focusable in case nothing is focusable inside (to make sure we can still trap focus)
- hide everything outside from screen readers (
aria-hidden
), we use https://github.com/theKashey/aria-hidden for this
- move focus to the first tabbable element inside the Dialog (or a more specific given element)
- whilst opened:
- trap focus inside the Dialog
- prevent outside clicks
- prevent outside scroll
- listen for escape key to close
- listen for clicks outside to close
- when closing:
- in most cases restore focus to previously focused element (or a more specific given element) — exceptions here are for example if clicks outside are not blocked, then we want to let the focus do its normal thing
Then, there is extra complexity added around the fact that these might be nested, ie. nested Popover
s like in Figma or Modulz, or even a Popover
inside a Dialog
, or Select
inside a Popover
, inside a Dialog
, etc…
For this we had a number of things going on:
- a focus trap manager which ensured only one was active at a time, making it way simpler to ensure only one level would do its thing when pressing escape, clicking outside, trapping focus, etc.
- some extra code in the react layer (
Lock
) to enable winding down a whole stack of Popover
s (Lock
s) when clicking outside and not preventing outside clicks (again coming from the needs of Modulz/Figma type apps).
Now
It's clearer than ever now, that whilst most of these are to be turned on for a Dialog
, for Popover
and others some features might be configured differently.
Since trying to add the ability to not trap focus in some cases (some Popover
), new things came up:
- should dismiss when blurring out of it
- should restore focus in normal tabbing order (even if inside a Portal somewhere else…)
Proposed approach
I suggest we try to untangle a few different things, which should hopefully help us with maintenance, readability, etc.
I think the hardest stuff we're going to face here is related to the nesting but also potentially race conditions once having separating stuff. I don't really have a plan regarding this, we'll have to see how it works and how we can make it work once split up.
For now, the things that need to be separate in different components I'd say are:
- focus management
- moving focus to first tabbable element OR element of choice
- restoring focus to previously focused element OR element of choice
- ability to not restore focus at all sometimes and let the browser do its thing
- focus trapping
- the approach we currently use works well using sentinel elements
- prevent outside click (new component)
- prevent outside scroll (new component, using
react-remove-scroll
already)
- listen for escape key (new component, need to see how we do nesting, etc)
- listen for click outside (new component, need to see how we do nesting, etc)
- hide from screen readers (new component, using
aria-hidden
)
Future
I think also perhaps another thing we need to discuss is how much of these features are applicable to each type of components, especially when it comes down to Popover
, Menu
, Select
, etc.