Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Focus management RFC #104

Closed
devongovett opened this issue Feb 15, 2019 · 39 comments
Closed

Focus management RFC #104

devongovett opened this issue Feb 15, 2019 · 39 comments

Comments

@devongovett
Copy link

This is an RFC for an RFC (ooh so meta 🤯). I'd like to get feedback from the React core team and the community on whether this is something that React should even handle before I do the work to come up with an API and write a formal RFC. Please let me know what you think!

Focus management

Focus management is the programmatic movement of keyboard focus in an application in response to user input, such as mouse, touch, or keyboard interactions. Implementing keyboard support for components and applications is imperative for accessibility, and not enough web applications implement this properly today. See the ARIA Practices document for more information about focus management and keyboard interfaces.

In React, implementing many common components is very easy, but implementing those components with proper support for accessibility and focus management is still quite difficult. Applications that do implement support typically use a component library which has taken the time to get this right. However, implementing such a library correctly is very challenging and time consuming, and I think React could enable library authors to handle focus management in an easier way.

Challenges

I work on the React implementation of our design system at Adobe. One of our main jobs is to implement a component library with proper support for accessibility, and we have faced some challenges while trying to achieve this goal.

Refs Everywhere

Refs are meant as an escape hatch from the declarative React model to make things happen imperatively, but we've ended up having one or more in almost every component in our library. The main reason we have refs is for focus management. We need to focus particular elements in response to events like keyboard navigation with arrow keys, etc. In addition, sometimes we need to find children of refs with querySelector and other DOM methods in order to focus the correct elements, e.g. elements inside child components. This feels pretty unclean, and un-reacty to us and I think there could be a more declarative API that React could enable.

Global focus state

The imperative focus management API in the DOM is global: only one element can be focused at a time. Calling node.focus() immediately causes all other nodes to lose focus, and the new node to gain focus. Therefore, components basically fight one another for focus. In reusable components, this is a challenge since they don't know what other components are on a page. There is no way for a component to "remember" what was focused before, so that focus can be restored to it later without some manual work in every component. This will be described more below in some of the other sections. I think React components could potentially do this automatically.

Roving tab index

Keyboard users typically navigate between components with the tab key, which changes the focused element. However, composite components such as lists, grids, trees, etc. should only appear in the tab sequence in an application once. It would be annoying if you had to tab through every item in a long list just to get past the entire list to the next element afterward. Therefore, you tab to the list, and if you want to navigate within the list you use the arrow keys, and otherwise you can tab to the next element after the entire list.

The roving tab index pattern is one way to solve this problem, but basically it involves each component being able to "remember" what was focused last, so that if a user tabs away from a list and then back, the list item they were on before is re-focused rather than the first item or the list itself. I think a focus management API in React could potentially help make this problem easier to solve.

Focus Trapping

Focus trapping is important for modals and other popups. It prevents users from tabbing or focusing elements outside a particular region. For example, if a user tabs to the last element in a modal dialog, when they hit tab again the first element of the dialog should be focused instead of something outside. This is currently impossible to implement in a general way in React without manual DOM querying and imperative focusing. I think a focus management API in React could help solve this.

Restoring focus

When you close a dialog or popover, focus should be restored to whatever had focus before the modal opened. This requires us to remember what was focused last, and when the component unmounts focus that element again. I think this should also happen automatically.

Why should this be in React?

We have experimented with various abstractions to make the above easier in user space, but have come to the conclusion that it would be better implemented in React itself. I haven't come up with an exact API that I think React should adopt yet (the purpose of this issue is to get feedback first), but generally, I think components should have a notion of whether they are in focus, and should be able to declaratively specify which of their child elements should have focus, and lock focus to within the component subtree. This has turned out to be difficult to implement in user space without access to the full React tree (only direct component children).

Most of the above points talked about the web, but I think an API in React core for this could be useful for other targets as well, such as React Native. React Native also has imperative APIs for focus management at the moment I think, so perhaps this could be valuable there as well.

Additionally, making implementation of proper accessibility patterns much easier for the entire React community could have a huge effect on the quality of modern web applications. Not everyone is using a component library that has invested as much time as we have into supporting accessibility. If more of this happened in React itself, then there would be less work for library authors and more people would be able to put in the effort.

Thoughts?

Sorry for the long issue. If you have thoughts or feedback please let me know. Is this something the React team and community would be interested in? If so, I'll work on an RFC for an API. If anyone else has ideas or has experience with this, please tell me in the comments!

@oriSomething
Copy link

Related even if limited:

Personally, it seems more specifically ReactDOM related. Not sure if it means ReactDOM's components, hooks or imperative API.

@theKashey
Copy link

Don’t forget about portals, which does break some ”dom” assumptions. While it still possible to support them somehow, as long as focus and blur events bubbles thought React tree - you can’t “tab” into them.

Related: facebook/react#14540

@jquense
Copy link

jquense commented Feb 15, 2019

@devongovett I'd be interested in what you think (broadly) such an abstraction or api would look like. I've also spent a lot of time building ui primitives in React (react-bootstrap, react-widgets, etc) and my experience definitely matches yours that focus management is hard and puts you soundly in ref-land everywhere. I've been mostly under the impression tho that browsers don't really offer the low level api's to do it well.

Maybe said another way, almost all my attempts to do broad focus management have resulted in very specific solutions for the use cases, because all of the imperative involvement needed to probably move and "stick" focus. I haven't had a sense that React knows more than i can about where focus is or should be, so while i'd love to see this in react, it's unclear to me whether React can help.

The other component here is the potential cost of such an abstraction. My experience as an app and library builder is that nitty-gritty focus mgmt is something most done in libraries. It's terrible and bad, but generally isolated, e.g. keeping focus in the right place inside a custom date-picker etc. Similarly focus trapping is annoying but solvable as a util, and with portals its easier b/c at least now focus bubbles up the react tree correctly for fixed position drop downs etc.

Sorry for the wall of text! This is something i think is super important and would love to see improvement on. I do think think we need to probably spec out what an API could look like and whether it's reasonable possible to implement. I also know the @sebmarkbage has had some thoughts around this in the past (i believe i'm remembering that correctly)

@devongovett
Copy link
Author

Personally, it seems more specifically ReactDOM related. Not sure if it means ReactDOM's components, hooks or imperative API.

Yep, react-dom would definitely be involved here, but I think the API could possibly be shared with other targets like react-native as well, which also supports focus.

Don’t forget about portals, which does break some ”dom” assumptions.

Good point. I think a react focus manager could possibly make that work correctly.

@devongovett
Copy link
Author

I'd be interested in what you think (broadly) such an abstraction or api would look like.

Ok, so this isn't very well thought through yet, but I think generally that components, along with elements, should have a notion of whether they are focused or not, i.e. conceptually there should be an activeComponent within React kinda like document.activeElement. They should be able to declaratively specify one of their child elements as "focused", whether or not the element actually has DOM focus. This would allow components to only need to worry about managing focus for their direct children, and not the rest of the world (see global state section above). The actual focused DOM element would be the focused element of the focused component.

This would also solve the issue of "remembering" what was focused last within a composite component (e.g. list) when tabbing to it or otherwise restoring focus. Additionally, when a component unmounts which currently has focus, focus could be restored to the previous component that had focus, and therefore to the correct child element (e.g. closing a dialog).

Along with a declarative API for specifying which child of a component should be focused, the API should allow for some common things:

  • Focusing a specific element.
  • Locking focus within a component's boundaries.
  • Restoring focus when a component unmounts.
  • Moving focus to the previous, next, first, or last focusable element (i.e. in response to the arrow or tab keys)

Those have been the main patterns we've needed to solve. There may be others, but I think those would get us a long way.

Again, people have implemented all of this in userland before using DOM APIs, but people had also implemented UIs before React. I think React could allow for a better API to make this easier and less bug-prone, like it did for the rest of UI development, which is why I'm proposing this as a feature. This is one of the last main things we have to go directly to the imperative DOM APIs for, and I find it unfortunate when the rest of React is so nice to work with.

@jamiebuilds
Copy link

jamiebuilds commented Feb 15, 2019

To get an API discussion started (Edit: Sorry the above comment hadnt loaded when I posted this), can anyone identify a primitive which might make implementing focus management significantly easier which is more generic than focus management itself?

What's the minimal missing piece of React which would make this problem space easier?

@devongovett
Copy link
Author

Thinking about this more, I think there is a conceptual focus tree that should exist. At the root is a FocusScope, which can contain FocusNodes and other FocusScopes. FocusNodes are elements which can be focused (e.g. input, button, select, etc.). These nodes form an ordered list within a FocusScope, which is used when tabbing around with the keyboard. Only nodes within the currently focused scope can be tabbed to. A FocusScope retains the state for its currently focused node even when it is not focused. When a focused scope is destroyed, it's parent scope becomes focused, and that scope's focused node receives focus.

As an example, imagine a page with a button which opens a dialog when clicked. Initially, focus is on the root scope, so you can tab around and get to the button. When the dialog is opened, the root scope loses focus, and the dialog's scope receives focus. You can tab around within the dialog, but not outside. When the dialog closes, and the scope is destroyed, the parent scope (root) becomes focused and focus is restored to the button.

This could be modeled in React like this:

function Dialog() {
  return (
    <FocusScope>
      <div className="dialog">
        <FocusNode><input placeholder="input 1" /></FocusNode>
        <FocusNode><input placeholder="input 2" /></FocusNode>
      </div>
    </FocusScope>
  );
}

It's almost possible to implement this using React context. You store the tree of scopes on the context, and FocusNodes register themselves with the nearest scope.

The problem with that, and the reason I think this should be part of React itself, is that focusable elements should automatically register as FocusNodes with the nearest scope, i.e. you shouldn't have to wrap them in <FocusNode> manually. Otherwise, we could use FocusNode in our component library's Textfield component, but if a user just used <input> manually then it wouldn't be focusable since the focus scope wouldn't know about it.

The best option would be if React by default registered the root focus scope on the page, and also exposed the <FocusScope> component for implementors of components like Dialog to register child scopes.

There also needs to be an API to control focus programmatically, but I haven't quite come up with that yet. It would involve possibly accessing the current focus scope and calling methods on it, e.g. FocusScope.current.focusNext() and FocusScope.current.focusPrevious(). I'm not really sure that's the best option though, and also haven't thought of a way to refer to a specific element to focus it other than refs...

@jamesplease
Copy link

jamesplease commented Feb 16, 2019

You can tab around within the dialog, but not outside.

I’m not a fan of React core dictating this behavior: I dislike the accessibility consequences. When a dialog in this situation isn’t active, a user can tab their way right out of the app and into the browser chrome (in many popular browsers). When the dialog is active, it prevents this native behavior, locking the user into the app.

I know this is probably an unrealistic goal, but I’d prefer to see a new API added to browsers that add something like what you’re describing, but one that doesn’t “lock” users into the page.

@devongovett
Copy link
Author

I dislike the accessibility consequences.

Ironic given that locking focus in a dialog is an accessibility feature to begin with. 😉 But, you're right, it's not ideal to prevent the user from tabbing out to the location bar or elsewhere. Perhaps there is a way to implement this so that can still be achieved, but even if not, I'd argue that preventing focus of other fields in the webpage e.g. behind the dialog is worse than not being able to tab out, which is why so many component libraries implement it this way.

I’d prefer to see a new API added to browsers that add something like what you’re describing

That would be awesome. I think we'd still need support from react though, in order to deal with things like Portals.

@jquense
Copy link

jquense commented Feb 16, 2019

It's possible to implement focus trapping that mimics the native dialog behavior, at least closely. Userland modals tho do need to do something, it's much worse to have no trapping from an a11y perspective, (SRs also have there own shortcuts for escaping to browser chrome so it's not necessary the worst if focus doesn't cycle out to the chrome. That said there is a browser API inert tho I think it stalled getting standardized

@theKashey
Copy link

‘Inert’ could be emulated with ‘aria-hidden’ - reach/portal is doing that. But you have to know which nodes you shall hide - “your” nodes, including portaled ones, should stay.

That’s said - we need an DOM abstractions, we could traverse. ReactDOM tree representing hierarchy from React poi t of view, including even something not yet rendered in a concurent mode.

Meanwhile, this data structure already exists(“fiber”) and used by many tools. But yet not documented.

@AlmeroSteyn
Copy link

Love when accessibility features get this kind of attention!

My initial feelings echoes that of @jquense :

Maybe said another way, almost all my attempts to do broad focus management have resulted in very specific solutions for the use cases, because all of the imperative involvement needed to probably move and "stick" focus. I haven't had a sense that React knows more than i can about where focus is or should be, so while i'd love to see this in react, it's unclear to me whether React can help.

Focus management is there to repair the standard DOM focus when we disrupt it with our code. In most applications there should be very little of that. Mostly router related, focusing error texts, the odd WAI-ARIA widget (which should really be built and tested into a library) and the odd modal (although the prolific use of modals is just as big an a11y issue as focus management). This does get a lot more complex the more complex the interface gets, though.

But the idea that React may try to actually focus things makes me slightly edgy. Hijacking the user's intention could in some cases be worse than not managing focus at all. So any such system should be opt-in if it is to do any focus setting itself and configurable to act in only certain parts of the application.

Think, for example, of the router. Trying to set focus to the actual content which may still be lazy loading while React is already overriding this focus to a previous element.

Another thing is that SR users can jump to entire other blocks. ARIA landmarks like

and and are exposed as quick navs via the screen readers. We'd need to avoid a situation where focus management becomes a race condition between AT and React.

We'd also have to consider the fact that React bubbles the focus and blur events, unlike the actual DOM.

I do like the idea of things like focusNext and focusPrevious as that can be pretty nasty to calculate resulting in, as you say, a whole soup of refs. Also the idea of activeElement and then caching the last focussed element, maybe something like prevActiveElement. But these are also, in a sense, imperative calls and the last two can probably be solved with hooks.

You can tab around within the dialog, but not outside.

I’m not a fan of React core dictating this behavior: I dislike the accessibility consequences. When a dialog in this situation isn’t active, a user can tab their way right out of the app and into the browser chrome (in many popular browsers). When the dialog is active, it prevents this native behavior, locking the user into the app.

I'd think that if a modal is complex enough for the user to want to tab out of it, it should perhaps not be a modal at all. Modals represent a complete context switch to the user and SR users should know they are in a modal and why. And aside from focus management the entire page content underneath needs to be set to aria-hidden="true". If it is just a screen in screen inception then rather use a loaded view and keep the standard behaviour? Again, overuse of modals causes serious a11y issues.

I don't mean to come across as negative, really just stating what I've experienced. If there can be a solution to make focus management easier while not getting in the way that would be great. I just don't see a lot of solutions for the the issues I know about in the proposal yet.

@devongovett
Copy link
Author

Focus management is there to repair the standard DOM focus when we disrupt it with our code. In most applications there should be very little of that.

Yeah, I think most users of React should never need to do their own focus management in application level code. Most of my job is implementing the ARIA widgets you mention though, so the main purpose of making a proposal for react core is to make this easier for us and other library authors. The easier this is to get right, the more libraries will do so.

But the idea that React may try to actually focus things makes me slightly edgy. Hijacking the user's intention could in some cases be worse than not managing focus at all.

I don’t think the intent is to take over the browser’s default focus management, but to allow an easier way for react components to move focus when it is actually warrented (see my original examples), and to make common patterns such as modals easier/possible to implement correctly (e.g. portals). We currently have to go to the native DOM APIs to do this, and that is hard to get right and feels dirty when the rest of react is so nice to work with.

@theKashey
Copy link

How "modals" were done in jQuery era

  • for every focusable outside of the target, set tabindex=-1
  • for every node, not containing target, set aria-hidden
  • in other words - reverse inert.
    And that could not be inert, or any other DOM-based solution due to portals, at least.

Probably we should do the same thing - "ask" ReactDOM or ReactNavite to apply some behavior settings to a tree, outside of the marked location.
Could it be something like Suspense - a special component which would instruct React to do something somewhere inside, or outside it's boundaries.
But that something should not be bound to focus.

@mshoho
Copy link

mshoho commented Feb 16, 2019

I have been working on a similar thing for ReactXP library (https://github.com/Microsoft/reactxp). It has an ability to restrict focus inside some container (for modals), limit focusability inside some container (for the list items so that you don't tab through the list forever), arbitrate focus when there is more than one candidate to be focused (the focus redirects might break the announcement of modals as the screen reader reads the modal title only first time you get inside).

There is an updated approach in the works which I do as a standalone package and plan to if a tree won't fall on me eventually opensource. It might be not what you need exactly, but at least it'll be a place for dumping the experience. There are lots of things we are used to never consider.

Generally, if you plan to allow the design system consumers to write custom HTML, the odds that their application won't ever be stable accessibility-wise are very high. As one focusable/accessible component outside the managed set of components might break the whole thing. Also computing the accessibility tree, the browsers imply a lot (and sometimes change the logic of this implication).

@AlmeroSteyn
Copy link

@devongovett sorry, I understood from the descriptions that React would then set focus itself:

Additionally, when a component unmounts which currently has focus, focus could be restored to the previous component that had focus, and therefore to the correct child element (e.g. closing a dialog).

That was a misunderstanding.

If it is localised and therefore opt-in so that library authors can use it but userland could carry on the way they did, it would be cool.

So your <FocusScope> will be the boundary then, if I get it correctly?

ReactDOM should be able to calculate all the focusable elements inside this scope just by checking tabIndex, so annotating every element should not be needed. Especially as the focus can be determined by the order of the HTML generated anyways.

Just speculating, but inside this one could then even have a useFocusScope hook or something that injects features like the prevFocusItem and nextFocusItem you mention?

to make common patterns such as modals easier

Just as long as one keeps in mind that modals by themselves can cause a11y issues. I'd would personally rather see it promoted to make things like tabbed interfaces, diclosures, accordions etc as they are not so horrid for accessibility as modals are.

@sebmarkbage
Copy link
Collaborator

I’ve been meaning to open up an RFC for a read api for these types of things that I do think belong in the core. I don’t yet have a concrete idea for the write part which is what this RFC would be about.

I definitely recognize the challenges that you presented and I think it’s worthwhile having it in the core if necessary.

I think there is a lot of overlap with focus, hover, touch-down pressing, selections and even VR gazing.

Often when you define a focus state you also deal with one of the other scenarios in the same component. So it’s important to consider how this all plays together.

That said, I’d definitely encourage keeping this going. An RFC doesn’t have to be a big deal so just start with what you got and we can discuss and iterate.

@theKashey
Copy link

There is a cool name for this the reactxp, @mshoho mentioned –

Restrictions and Limitations API

  • focus lock
  • scroll lock
  • event lock
  • view lock
  • mouse lock
  • ...
  • ...

@devongovett
Copy link
Author

I'm currently working on writing an RFC for this. I should be able to submit it early next week.

One question I ran into is regarding tab order and portals. Because of portals, the order of elements in the React tree may differ from the order in the DOM. This affects tab order because the browser doesn't know the original order from React. This can be easily seen with the following example:

function App() {
  return (
    <div>
      <input placeholder="input 1" />
      <Portal>
        <input placeholder="input 2" />
      </Portal>
      <input placeholder="input 3" />
    </div>
  );
}

The tab order in this example will be input 1, input 3, and then input 2, assuming the portal is placed after the app in the DOM. Would you expect that behavior or do you think the tab order should be what is defined in the React tree (input 1, input 2, input 3)?

@dantman
Copy link

dantman commented Feb 22, 2019

Would you expect that behavior or do you think the tab order should be what is defined in the React tree (input 1, input 2, input 3)?

Very often portals are used for modal dialogs. And because we don't currently have a good way to imperatively "emit" a dialog, they often get dropped into the React tree next to the button that opens them. So I do think it usually makes sense when the tab order follows the DOM order not the React tree order.

e.g.

function App() {
  const [dialogOpen, setDialogOpen] = useState(false);
  return (
    <div>
      <input placeholder="input 1" />
      <Dialog open={dialogOpen}>
        <input placeholder="input 3" />
      </Dialog>
      <button onClick={() => setDialogOpen(true)}>input 2</button>
    </div>
  );
}

There are a few cases where Portals are used for local things like tooltips, and I suppose popover bubbles. But I can't come up with any concrete examples where something in a Portal is both local and would actually contain something you would focus.

Though if there are any special cases where you would want tab order in a portal to follow React tree order instead of dom order, we might want to have an opt-in behaviour for that.

@devongovett
Copy link
Author

Yep, modals are a separate case because you want to lock focus within them. That will be a separate part of the RFC.

Cases where you might want to be able to tab in and out of portals are things like popovers, dropdowns, etc. If the portal in my example above were a popover, then tabbing out of the popover should probably go to the next input rather than to the browser chrome or whatever happens to be next in the document body.

Also, portals can be non-deterministic. Their order depends on the order they are appended to the DOM, along with the specific implementation of the portal component that is used. If that implementation changes, than the tab order of the page could also change. If the tab order were based on React tree order, then this would not be an issue.

Think also of nested portals, e.g. a dropdown in a modal. They are nested in the React tree, but not necessarily in the DOM tree. This could cause the tab order to be inconsistent and non-intuitive.

FYI, I'm running a poll on Twitter here for what people would expect. Please vote. https://twitter.com/devongovett/status/1099039096867934208

@StommePoes
Copy link

I think that poll doesn't offer enough information, because it's at the JSX level. I skimmed this page and think I agree that when the browser can't keep track of things, someone else does need to. Our problem we've created is that these reusable components don't "see" each other.

One problem we had with our reusable react components at $previousJob for instance was the following:

A menu (following aria-menu pattern of a triggering button and a menu dropdown with menuitems inside) has a menuitem whose job when clicked was to open a typical modal dialog.

The modal was a separate component who did all its own focus management and had all the right aria attributes and whatnot. However, its problem was where focus went when the user closed that modal.

The original trigger is now gone, because the dropdown only knows it lost focus, and this means it should close. In this case, the actual whole menu vanishes (other teams will simply make it display: none... same difference for the user though). Who gets the focus? We believed the menu's triggering button (who opened the dropdown) should get focus, but the modal couldn't know who that was (the modal's original design would take note of who the event target was when it launched open because we wanted developers to be able to use the modal without hooking that stuff up themselves-- one of the reasons for reusable accessible React components was that other teams in the company could grab them and run, and didn't need to have the Aria Authoring Practices 1.1 memorised, or don't get the opportunity to screw things up).

But now we can't "see" our trigger, and need to assign a new one. Someone outside both components (I'm still not sure if it should be a Global anything but... might make sense) needs to be able to see the big picture, and it can't be the browser. I suppose it would make sense that whatever Javascript tool being run needs to do it.

This Global whatever needs to be really accurate though. Part of the problem will be the various edge and not-so-edge cases where webby people cannot decide what the correct action is, and users won't either because they're not always common enough patterns, as well as users treating the web differently than they treat an application they open on their desktops.

So perhaps some proofs of concept need to be built (and I mean running somewhere where people can interact with it, esp with assistive technologies) and tested by accessibility people (and likely the people writing the ARIA specs specifically), while in the meantime the Authoring Practices can be the guide for a lot of the basic popular cases.

So... cautiously optimistic on the idea, and completely agree that it's a problem that needs to be tackled in some way. But the important part won't be the JSX. It'll be the end result. I don't think anyone can look at the example in the Tweet poll and give any correct answer.

@AlmeroSteyn
Copy link

then tabbing out of the popover should probably go to the next input rather than to the browser chrome or whatever happens to be next in the document body.

If the input is rendered in the position it is declared in then yeah. If, however, the portal is rendered elsewhere focus should go to whatever is next in the DOM. Otherwise keyboard users could end up jumping around the page "randomly" and screen reader users will probably lose context.

If the portal was rendered in another place than in the normal DOM flow there is a specific design reason for it. If focus is sent to the portal then, there should then be a clear escape path for the user. But, it should be the user's decision. The user should still have the option to browse further OR return to the origin of the event in non modal cases.

Think of an error block for a validated form. Sending focus to the error block will allow the user to navigate to any of the controls in error, or simply tab out of it and continue with the top of the form again if they choose so. If I did render this with a portal and focus got sent back to the next element in the React tree I would take away the user's option to continue browsing in cases like there.

Been wracking my brain but all cases where I want to send focus back to the origin are either modal in nature or the kind of functionality I want to render at the point of origin anyways.

In your experience what are some concrete use cases that fall outside of this?

@dantman
Copy link

dantman commented Feb 23, 2019

If the portal was rendered in another place than in the normal DOM flow there is a specific design reason for it.

Very frequently that reason ends up being "if I render it locally then it will get clipped by overflow or position calculations won't work right". All of Material UI's local menus are located at the top level for this reason.

@jquense
Copy link

jquense commented Feb 23, 2019

I don't think portal behavior with focus is generalizable for all portals. But definitely in the drop-down sort of cases where Portals used to deal with needing position: fixed to escape inline overflow and related almost always should act as if they were in line in the DOM in terms of focus.

Tbf tho in those situations you're usually building a form input and need to explicitly manage virtual focus while keeping "real" focus in a textbox or something anyway

@dantman
Copy link

dantman commented Feb 23, 2019

Tbf tho in those situations you're usually building a form input and need to explicitly manage virtual focus while keeping "real" focus in a textbox or something anyway

You are thinking about select style dropdowns. There are also context menus opened from buttons. e.g. A triple dot menu. For these the menu items most definitely are focused.

@jquense
Copy link

jquense commented Feb 23, 2019

True menus are usually focusable in the traditional sense 👍

@theKashey
Copy link

Portal is an implementation detail and should not count. You should be able to replace select with Select and keep the old tab behavior, regardless of how it got rendered.

@AlmeroSteyn
Copy link

Very frequently that reason ends up being "if I render it locally then it will get clipped by overflow or position calculations won't work right". All of Material UI's local menus are located at the top level for this reason.

These are indeed compelling examples. Thanks!

In these causes you would indeed want the focus to behave as if it was rendered inline.

But this is, of course, exactly what focus management is for, repairing focus that has been disturbed. However I don't see a way that React would know that one particular box should be virtual inline and another one not.

So the way I see it is that the developer remains responsible for deciding if and when focus should be adjusted with this RFC improving the base tools with which this gets done.

Timeouts

Another thing that perhaps should be mentioned and considered here is the current need for timeouts in some cases.

I don't have the exact conditions nailed down yet but sometimes I need to wrap focus setting in timeouts to allow for DOM updates to complete and AT to recognise the update between everything else that is happening. So I get this:

setTimeout(()=>{
      someRef.current.focus();
});

There are also cases where the timeout need to be in the 80-200 ms range to be picked up by AT. VoiceOver seems to be particularly sensitive to this.

Another place this is happening are hash URL's when opening them directly in the browser. So when I have a URL like:

http://somedomain.io/documentation/accessibility#setting-focus

Where I then have in the page:

<h2 id="setting-focus">Setting focus</h2>

When navigating in-app these tend to work nicely but when copy and pasting this into a new browser session or hitting refresh, I am often back in setTimeout land. I believe it to be that the browser is doing it's native focus setting when the DOM is still being rendered and not stable yet.

So I am wondering if all this is not something that could also be addressed in this RFC. I think with async rendering this could become even more apparent, so if React could be more helpful in this it would be great.

@devongovett
Copy link
Author

I was thinking that making portals behave like they are inline would be a better default. A separate API could be used to lock focus within a container. I'm not sure the current behavior of making the tab order dependent on the order portals are appended to the DOM is ever the correct behavior. Either you want the tab order to be inline (menus, some popovers, etc.), or you want to constrain it (dialogs, other popovers, etc.).

@devongovett
Copy link
Author

Also making the tab order behave like the portal is inline would match other portal behavior, like event bubbling.

@devongovett
Copy link
Author

RFC submitted here: #109. Rendered. Please feel free to leave feedback on the PR.

@ogonkov
Copy link

ogonkov commented Feb 27, 2019

I personally solve roving tabindex problem with Provider, that wraps elements that should be managed, with checks for currently selected element on Consumer level

@arieh
Copy link

arieh commented Feb 14, 2020

Just wanted to add that I find this to be a super important topic. Any application that is above medium size and wants to be accessible will run into a lot of difficulties when trying to create consistent usable keyboard nav.

Since solving this in userland usually means using an external component library, it will likely mean most mid-sized (and larger) apps will default to not being accessible.

Not sure how such an API will look like, or if an "easy" implementation to such a problem can exist, but am super excited to see a discussion on this front take place.

@quantizor
Copy link

Looks like @devongovett mostly implemented this in https://react-spectrum.adobe.com/react-aria/FocusScope.html if y'all want to use it today 💯

@gaearon
Copy link
Member

gaearon commented Aug 24, 2021

Gonna close the issue but we're keeping the RFC open and at some point will get back to it.

@gaearon gaearon closed this as completed Aug 24, 2021
@givebk-bot
Copy link

Hey! Looks like someone invited me to this issue.

@antl3x
Copy link

antl3x commented May 14, 2022

@givebk-bot !donate @devongovett $ 1

thanks for the FocusScope! ❤️

@givebk-bot
Copy link

givebk-bot commented May 14, 2022 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests