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

RFC: Focus Management API #109

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open

Conversation

devongovett
Copy link

This RFC proposes adding a builtin FocusScope component and FocusManager API to react-dom, along with internal logic to properly manage focus in React applications. Along with focus scopes declared in the application, it will take into consideration portals and other React concepts which may cause the React tree and the DOM tree to differ in relation to focus management.

Related discussion: #104.

View Rendered Text


## FocusManager

In addition to defining the focus scopes in an application, components need an API to programmatically move focus around. The singleton `FocusManager` API exported by `react-dom` provides this interface.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would this work with multiple React roots on the page?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only one root can have focus at a time. The FocusManager APIs would apply to the root (and scope) which currently has focus.

@theKashey
Copy link

You are mixing 3 separated things

  • Trapping focus (which, so far could be implemented in user space), with return focus, but without autofocus.
  • Controlling, which is not quite "component" friendly. Focus Manager should not allow controlling focus outside of a current component, and should provide API to move a focus into the current element. Plus the difference between focusNext and focusNextTabStop isn't clear. I am not sure this API should be exposed, at least in the beginning.
  • Traversing. Ie managing portals and digging some information from a React Tree. This should be a separate RFC, as long as there are other tasks, like a cousin ScrollLock, which requires the same structure to traverse.

@devongovett
Copy link
Author

devongovett commented Feb 27, 2019

I think all of these are related, and should be considered together.

Trapping focus can kind of be implemented in user space, but not fully. It's not possible to support portals correctly.

Controlling focus works within the scope, not the component. In a compound component like a list or table, you might want to handle keyboard events at the top and move focus in response. That might be several levels deep rather than a direct child of the component (e.g. within a row and cell). I think it would be inconvenient and not much better than we have now to have to do that focus marshalling manually.

The difference between focusNext and focusNextTabStop is that focusNext focuses the next focusable element in the scope, whereas focusNextTabStop focuses the next tabbable element in the nearest locked scope (or the root scope). There are definitions of those terms in the RFC. I'm open to better naming and methods for the FocusManager API though, as long as both options are possible.

Traversing is quite related to all of this as well. I suppose it could be separated but it would end up being implemented together so I thought it would be better to consider all of this holistically.

@theKashey
Copy link

Trapping focus can kind of be implemented in user space, but not fully. It's not possible to support portals correctly.

Possible, but might require deeper integration into the browser (read - hook on Tab key)

Controlling focus works within the scope, not the component.

I mean - component and everything below. Not above. For example - "focusNext" on the last element should move focus to the first element of the current scope, not to the "next" in the document scope.
As you written - Once focus is inside the scope, components can use the FocusManager API to programmatically move focus within the scope.

The difference between focusNext and focusNextTabStop is that focusNext focuses the next focusable element in the scope, whereas focusNextTabStop focuses the next tabbable element in the nearest locked scope (or the root scope).

Probably we shall not rely on tab index here, and use some other abstraction. For example - in Safary Links and Buttons are Tab and Option-Tab focusables. It's like two different groups of tabbables, and we, probably, should not affect browser behavior (just imagine - there is no React on the page)

Traversing is quite related to all of this as well. I suppose it could be separated but it would end up being implemented together so I thought it would be better to consider all of this holistically.

Travesing is needed for other tasks, and should be considered in a broader scope.

@devongovett
Copy link
Author

I mean - component and everything below.

Right, that's defined by rendering a FocusScope. And yes, focusNext cycles within the current scope, not outside.

@AlmeroSteyn
Copy link

Read through the RFC and great to read something that is so detailed and well thought out.

There are some things I really think will help. As I read, the following questions came up. Some of this I have mentioned before but for clarity will mention it again here.

Focus trapping

For me the idea of focus trapping is pulled broader in the RFC than it should. Stating the obvious, but focus trapping is the keyboard equivalent of not being able to click on anything else for a mouse user. If the keyboard gets trapped while mouse and touch users can still navigate other elements, it is no longer inclusive design.

So it seems reasonable that when focus is trapped, there is one container trapping focus and it is modal, i.e. any user cannot interact with anything else on the screen aside from what is in the trapped container.

Even if we Inception all the things and have modals inside modals, only the current active group of elements should be navigable.

Setting tabIndex="-1" on everything that falls outside the focus trap will potentially create partial keyboard traps for keyboard users as other users can still freely interact with everything. Instead one would typically create an overlay so that mouse and touch users can't interact with the background. And we then need to add aria-hidden to the parts that do not form part of the focus trap for screen reader users.

This is still an imperative task. So I feel that the RFC is helping us halfway here while still leaving some imperative tasks to the user. This makes me wonder of it should not form part of the focus trap implementation.

Auto focus on unmount

I don't think that React should automatically set focus to the previous FocusScope when a current one unmounts. As @theKashey mentioned above, I think it should return the suggested focusable leaving it up to the developer to set focus or not. Otherwise it will probably create focus race conditions in cases where the predicted focus is not correct. Or in cases where that element no longer exists. For example if you opened a modal from a menu item and the menu auto-collapsed.

I do not think that outside of handling focus-setting events, React should ever do the actual focus setting. Leave that to the implementing developer.

Positive tabIndex

Although I understand why you propose using this, I would plead for another way.

As you state: it could be controversial. In fact this practice is strongly condemned in every accessibility document that deals with this subject matter. It is also heavily validated against in automated accessibility tools: You already mentioned aXe-core , but also in others, including our test API. This means that React applications will suddenly start creating unfixable a11y findings in audits. Which would be a first and therefore odd coming from an RFC that aims to increase accessibility.

On top of this, developers taking a cue from React on how to implement their own things could start implementing positive tabIndex elsewhere.

I believe that React should either be impartial to how a11y features are implemented or, when it does become opinionated, follow the specifications and recommendations of the a11y community.

I also share the concern of what it would do in mixed-mode applications.

When it comes to implementation of the positive tabIndex I did a quick test with Firefox and NVDA.

In a barebones create-react-app application I did the following:

 <div className="App">
        <label htmlFor="input1">Input 1</label>
        <input id="input1" tabIndex="1" />
        <label htmlFor="input3">Input 3</label>
        <input id="input3" tabIndex="3" />
        <label htmlFor="input2">Input 2</label>
        <input id="input2" tabIndex="2" />
        <label htmlFor="input4">Input 4</label>
        <input id="input4" tabIndex="4" />
      </div>

With NVDA on, the keyboard interaction in Focus Mode followed the tabIndex, as expected. In Browse Mode, however, the tabIndex order is ignored again and the elements are read out as they are encountered in the DOM.

Also, when opening the rotor the elements are listed based on their DOM position and not their tabIndex:

Elements list in NVDA

As a sighted user I am a mediocre screen reader tester at best, but I cannot see the benefits of autocalculating tabIndex over just repairing focus. Am I missing something here?

@devongovett
Copy link
Author

devongovett commented Feb 28, 2019

@AlmeroSteyn thanks for your insightful feedback! Some responses below.

Setting tabIndex="-1" on everything that falls outside the focus trap will potentially create partial keyboard traps for keyboard users as other users can still freely interact with everything.

Yeah if used incorrectly. However, everything can be used incorrectly. I'm basically proposing react set the equivalent of inert on elements outside the locked focus scope. It's still on authors of e.g. dialog components to add a backdrop to deal with clicks etc.

I don't think that React should automatically set focus to the previous FocusScope when a current one unmounts.

You're right that I left out some details of this. What happens if the previously focused element doesn't exist anymore? Sometimes we cannot know where focus should go automatically. However, I think it's a reasonable default. Perhaps we could support onFocus on the FocusScope itself, which would allow the user to do something custom when the scope receives focus, like re-target focus to a different element.

For your menu case, I think what would actually happen is that when the menu closes, its focus scope would unmount and restore focus to the target that opened the menu (e.g. button). When the dialog unmounted, it would restore focus to the button. So it might still just work by default in that case. But you're right that there will always be cases where we cannot predict the correct item, and for those we'd need a hook to let the user override.

With NVDA on, the keyboard interaction in Focus Mode followed the tabIndex, as expected. In Browse Mode, however, the tabIndex order is ignored again and the elements are read out as they are encountered in the DOM.

That's interesting information. I am very aware of the issues with positive tabIndex and hesitated strongly about suggesting it as an implementation strategy. I was worried that reimplementing browser behavior by overriding the Tab key would be worse. We could maybe handle the tab key correctly, but perhaps it works differently across platforms or browsers or something.

Also, what about hardware that doesn't have a tab key, like touch screen devices, or game consoles? In those cases, tabIndex may be the only way to expose the information about tab order that we know to the platform.

Positive tabIndex would only be needed in a very narrow case: when portals are used, and while focus in inside a React root. That should work around the issue with mixed-mode applications as you say.

That said, if there is a better way of implementing this that I'm just unaware of, please let me know.

@AlmeroSteyn
Copy link

Yeah if used incorrectly. However, everything can be used incorrectly.

Granted! As long as any changes React make to the elements give an inclusive result then all good.

You're right that I left out some details of this. What happens if the previously focused element doesn't exist anymore? Sometimes we cannot know where focus should go automatically. However, I think it's a reasonable default. Perhaps we could support onFocus on the FocusScope itself, which would allow the user to do something custom when the scope receives focus, like re-target focus to a different element.

I like the idea in the last sentence here. An event that gives the use the ability to do something with it.

I would still not want React to decide for me when and where to go set focus, but rather have React give me the tools so I can easily do it. If I have an event indicating that focus should be set and something else (FocusManager) that gives me the tools to easily determine what the expected default is, I am in the position to use it or not. This sounds like a good default to me.

Also, what about hardware that doesn't have a tab key, like touch screen devices, or game consoles? In those cases, tabIndex may be the only way to expose the information about tab order that we know to the platform.

Should we not look at it as the effect caused by moving focus. Whether you use Tab or arrows or swipe to move focus is inconsequential, rather that the result is correct.

So I have some event that tells me focus is about to move and I have a destination I need that focus to go to which may be different from the next in the DOM order and then I act.

So in a modal, activating the button that opens it would be the event trigger to move focus into the modal and the close action of the modal would the trigger to return focus to the controlling element. I don't see that it matters where this action comes from, the fix is to repair focus when it is disturbed.

I would expect this to still work in portals?

That said, if there is a better way of implementing this that I'm just unaware of, please let me know.
I was worried that reimplementing browser behavior by overriding the Tab key would be worse.

For fairness I confirmed my suspicions with our own auditors at Tenon before answering. Out of that discussion a pretty real world use case came up: If you set positive tabIndex it does not stop a screen reader user from quickly navigating to other areas of the page, unless the entire thing is aria-hidden="true". This user would then expect the taborder to continue from this place, however, positive tabIndex could likely hijack that and throw the user back to where they wanted to navigate away from. This would be the case for any non-modal portal.

Not to mention that React will need to keep track of all managed and unmanaged (parts that may not be coded in React) tabIndexes on the entire page to calculate an order that would work. Otherwise very unexpected results could arise.

Based on these kind of unexpected behaviours sites that use positive tabIndex will more than likely not pass audits. Which, as I mentioned before, would be the first time that something internal to React fails a11y audits. If React starts doing things that causes unfixable audit findings this could mean that React gets dropped from projects where this required.

So I am really excited about some of the features in this rfc but it really needs to be solved without tabIndex :-(

That said, if there is a better way of implementing this that I'm just unaware of, please let me know.

Just from my experience with refs and focus management I think aiming to make repairing focus easier with something like FocusManager could be enough.

As mentioned above I cannot yet see why this would not work for portals.

Perhaps a first pass of the rfc should focus on tools making it easier for the developer while leaving the actual actionables to the developer. Once that is nailed down and working it would be easier to crystalize out the bits where React could potentially do a little more. What do you think?

@sylvhama
Copy link

sylvhama commented Mar 1, 2019

At Ubisoft we have developed a simple Focus Manager to handle gamepad navigation in our SPA used on X1 and PS4. Sadly I've not succeeded yet to open source our solution. I will look at your RFC and try to help as much as I can. You can have a look to a pres I've made with some code previews: https://github.com/sylvhama/bringing-the-www-to-the-aaa

@devongovett
Copy link
Author

devongovett commented Mar 1, 2019

I would still not want React to decide for me when and where to go set focus, but rather have React give me the tools so I can easily do it.

The point of this RFC is so we can stop manually managing focus imperatively, and just declare what we want to happen. I agree that we might need manual overrides for some edge cases, but I think we can come up with a good default that works automatically for most uses.

If you set positive tabIndex it does not stop a screen reader user from quickly navigating to other areas of the page

That's fine, and expected. We will be setting tabIndex="-1"/aria-hidden/inert on everything that shouldn't be focused (outside the locked focus scope), so the screen reader will ignore those elements. Positive tabIndex will be set only for elements that can be focused.

Just from my experience with refs and focus management I think aiming to make repairing focus easier with something like FocusManager could be enough.

Sure if you are just repairing focus, but that's not possible without some context about the user's intent. In the portals example in the RFC without the positive tab indexes, focus would go from input 1, input 3, input 2. If you wanted to repair that, when input 3 focused, you could instead move focus to input 2. However, you only want to do that if it came from the tab key or some other focus movement interaction. You don't want to repair it if the user directly clicked or tapped on input 3.

So, we need some info on the user's intent. The tab key is just one of those, but I don't think it's the only one. From @sylvhama's presentation, it looks like game consoles move focus around using some non-standard key codes. I'm not sure if mobile browsers with previous/next buttons to move focus fire keyboard events or not. And there are probably more. The point is that we can't reproduce this reliably in JS. It has to be done by the browser. And the only way to expose information about the correct tab order to the browser as far as I know is via positive tabIndex.

We can perhaps prototype this and test it out across platforms/browsers/screen readers, and see how bad it is. If it's just causing issues with audits, then those audits should probably be improved. Blanket banning a browser feature because it can be used incorrectly doesn't seem very productive. If it is causing actual issues with usability, and we determine that it isn't possible to implement the desired behavior, than perhaps we will need to drop that part of the RFC.

@theKashey
Copy link

It sounds like - forget about the Tab key.
In the react-focus-lock I am not emulating the Tab, but just observing focus behavior:

  • onfocus and onblur events could trigger a check to return a focus where it should be
  • there are a special markers before and after lock, to track sequential tabbing - you are tabbing into the "trap" and it trigger a check
  • the same traps could be placed around portals(drop downs) - div focusable divs (add tabindex) - once focus it's looking for a portal inside and moving focus to a distant node

So - it has nothing with emulating focusing, playing with tabindexes and intercepting browser behavior - just observing the focus related events, and nothing more.

@devongovett
Copy link
Author

the same traps could be placed around portals

Yeah that's possible, but I don't think it's a good idea for react to be inserting extra DOM nodes for you that you don't render. That could potentially mess up things like CSS selectors that depend on the structure you actually render.

@theKashey
Copy link

theKashey commented Mar 2, 2019

So - there are three options:

  • tab in the browser defined order (portal last). This is how it works today.
  • override browser behavior for tab (add here gamepads and any other system). That's not so bad, but very fragile.
  • team up with a browser, and probably it's the best way.

Why it would not lead to the issue you've pointed on - because any nested node would not exist - it's portaled!
But that would mean - if you want you portal to be "transparently tabbable" - you should wrap it with some special markup - ReactDOM.forwardPortal, which may create an invisible, not tabbable node, just to catch a user focus.
Even more - that invisible node might contain a reference to a portal destination, to let other tools teleport without use of React internals.

const forwardPortal = (children, targetNode) => (
  <div data-portal={targetNode} styles={portalTrapStyles}>
     {ReactDOM.createPortal(children, targetNode)}
  </div>
)

Done! In a user space!

@AlmeroSteyn
Copy link

That's fine, and expected. We will be setting tabIndex="-1"/aria-hidden/inert on everything that shouldn't be focused (outside the locked focus scope), so the screen reader will ignore those elements. Positive tabIndex will be set only for elements that can be focused.

A locked focus scope should be the result of a user action. They should never just occur during normal navigation as this wil be a serious a11y violation in itself. So a user activates some trigger. This is modal behaviour and setting the rest as inert/aria-hidden="false" supports that. So if you want React to do this all it will need to be aware of the trigger anyways. Once the modal closes that is another event. In both cases focus can be repaired.

There are also cases where the popup is not modal. A menu is a perfect example. One simply cannot make the rest of the application inert just because a menu is open. It would be as good as placing the menu on a new page and navigating there for screen reader users. So just like every other user, the screen reader user could and should be able to interact with the rest of the page. If they decide to jump to an ARIA landmark such as a <nav> at the top of the page or a <footer> at the bottom, the positive tabIndex will probably mean that the user's next Tab is hijacked and pulled into the menu again.

A solution based on repairing focus may require more intervention from the developer, sure, but it is 100% guaranteed not to suffer from this. I like so much of what this RFC suggests that the support it would give me will already be such an enhancement even if I have to be the one to still do the focus setting.

We can perhaps prototype this and test it out across platforms/browsers/screen readers, and see how bad it is. If it's just causing issues with audits, then those audits should probably be improved. Blanket banning a browser feature because it can be used incorrectly doesn't seem very productive.

If I came across as advocating a blanket ban of a normal feature or saying we need to avoid this simply to please audits, I am sorry. This was not the intention. Allow me to clarify. If I say things you already know, apologies, but I think it should be on record here.

Browsers have functions that have proven to be harmful for accessibility, like the <marquee> tag. From the wealth of support, positive tabIndex appears to be another one.

Far be it from me to say that things cannot be improved. The WCAG and a11y audits are changing. In fact we just saw that with version 2.1 of the WCAG. However this document and the audits that stems from it comes from many years of testing and encompasses a huge range of combinations of users, user agents and assistive tech. It help us so that we do not have to do this testing every time ourselves. Good audits from reputable experts dig into this knowledge to assist, not to dictate.

If React becomes opinionated about something that every good accessibility source advises against, the testing base will need to be super huge. This means users with specific disabilities testing in every user agent known with every possible form of assistive tech. So it needs to cover screen readers, single switches, motion tracking, eye tracking, speech recognition... the list goes on. Once that is all tested and works for all cases that users can come up with using the new things this RFC aims to create it will be in a position to contradict the burden of evidence out there that positive tabIndex is a bad idea.

Keep in mind that right now I can use React to create a website that conforms to all accessibility requirements out there. This is an incredible strength and is because React stays impartial to controversial accessibility issues and leaves that to user land.

Some references on (positive) tabIndex:
https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex
https://developer.paciellogroup.com/blog/2014/08/using-the-tabindex-attribute/
https://webaim.org/techniques/keyboard/tabindex
https://dequeuniversity.com/rules/axe/3.0/tabindex
http://www.karlgroves.com/2018/11/13/why-using-tabindex-values-greater-than-0-is-bad/
https://developers.google.com/web/fundamentals/accessibility/focus/using-tabindex

@jquense
Copy link

jquense commented Mar 2, 2019

I'm not sure I understand the problems with Portals and focus flow for traps. In all focus traps I've implemented you listen at the root
element and check for blurs that aren't paired with a bubbled focus. This works for portals too since events bubble through the component tree not the DOM tree

@devongovett
Copy link
Author

devongovett commented Mar 2, 2019

@AlmeroSteyn

There are also cases where the popup is not modal. A menu is a perfect example.

Yeah, and in that case you wouldn't use the lock prop on the FocusScope. Then the rest of the app wouldn't be inert.

In reply to the rest of your response, I understand the issues with positive tabIndex that you keep repeating, but no one has offered an alternative suggestion that would be able to implement what we need in a reliable way across platforms. This isn't about locking focus - that can be done without positive tab index - this is about correcting the tab order when a user tabs into a portal. If someone can come up with an alternative implementation suggestion to fix the example in the portals section of the RFC, I am all ears! But as far as I can tell, positive tabIndex is the only way.

Again, this is only necessary when a portal is present, and the user is focused within the react root. This way it shouldn't cause usability issues with the rest of the page. However, if we determine through prototyping that it does still cause problems, and there is no other way to implement proper tab ordering through portals, then we may just have to drop that part of the RFC, leave the behavior as it is today, and work with browsers to make it possible to implement reliably.

@jquense

I'm not sure I understand the problems with Portals and focus flow for traps.

Focus flow in portals doesn't have to do with traps at all, just in how focus flows around portals in general.

For traps specifically, you need a way to query whether an element is "inside" a FocusScope, which is why it needs to be done in react itself. Checking for blurs without a focus doesn't seem reliable - what if the user is actually blurring and not moving focus? Then you'd get a blur event without a focus, and cause focus to move even though it wasn't the user's intent.

@jquense
Copy link

jquense commented Mar 2, 2019

Checking for blurs without a focus doesn't seem reliable

not to sideline the conversation on a specific thing, but it is reliable, if you are trapping focus in a particular div and you get a blur bubble up without a subsequent focus then an element outside the div was focused or focused dropped out of the window, e.g. you can't have "just a blur" focus always moves somewhere.

In any case my point was more about RFC scope, it's not obvious to me that focus traps are something React can implement better than i can already, even with portals. THat's fine too, i'm happy to not have to implement stuff, but do want to get clearish on the line between "what is possible to implement in a browser", "what is possible for React to implement", and finally "what is possible for a user of React to implement in the context of React". It helps understand which aspects are worth bike shedding vs others is all.

Awesome work tho, really happy to see this have such lively discussion!

@devongovett
Copy link
Author

Imagine a dialog. It contains some inputs.

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

If input 1 is focused, and a user then clicks outside either of the inputs, whether that's still somewhere within the dialog or completely outside, the input will blur and there will be no corresponding focus event. Then you'll end up moving focus back to one of the inputs, which would be wrong. Also, based on that information, how would you know which input to focus? You don't know if the user is moving forward or backward in the tab order. Do you have an example of an implementation of this working somewhere I could see?

@jquense
Copy link

jquense commented Mar 2, 2019

Then you'll end up moving focus back to one of the inputs,

I think we have different expectations of behavior for a focus trap. If you blur off an input the focus should drop to "body", in this case the outer div, not the first or last focusable item. If you tab out of the trap (forward or backwards) focus should move to the browser chrome. It should be noted that with a native dialog, blurring to nothing moves focus to the actual body (demo not in an iframe)

In any case I did confuse my various implementations :P the most common one i've employed is in react-overlay's Modal: https://react-bootstrap.github.io/react-overlays/#modals which definitely isn't perfect, and does suffer from not supporting portals b/c it listens to the document directly (something i've been meaning to fix). So i'm sympathetic here! I just often feel that the limitations polyfilling this are more browser related than not-enough-hooks-into-react related.

@AlmeroSteyn
Copy link

@devongovett

In reply to the rest of your response, I understand the issues with positive tabIndex that you keep repeating, but no one has offered an alternative suggestion that would be able to implement what we need in a reliable way across platforms. This isn't about locking focus - that can be done without positive tab index - this is about correcting the tab order when a user tabs into a portal. If someone can come up with an alternative implementation suggestion to fix the example in the portals section of the RFC, I am all ears!

You are right, I have said my peace here.

As for an alternative implementation, no I don't have one and am personally not phased that as a developer I have to still do something here.

Because focus is only a part of the story. In the case of the menu I have to know to add aria-haspopup="menu" to the trigger and to add aria-expanded="true | false depending on the state of the popup (or portal in this case). Then I may want to add aria-controls to the trigger which will mean generating a unique page ID for the portal. (Although there is controversy regarding aria-controls : http://www.heydonworks.com/article/aria-controls-is-poop).

If I am coding a modal my ARIA states and properties are different again so that rules out having them set by default.

So if the FocusManager has the next focusable ready for me, using it is a small extra trouble while setting up the proper ARIA. At this stage it would already be a great help and cut out a LOT of code and refs juggling.

Stab in the dark: You mentioned the need for more declarative focus setting. What if FocusScope carries something like a focusOn prop that can take a boolean value or a function. Then I can still reasonably declaratively set focus, and can tie it to the same actions that would set my ARIA?

In cases of tabbing into an open portal, this could also possibly be solved with such a focusOn prop as the user can then define the exact parameters for entering the open portal. But IMO non modal popups that stay open should really be the exception as, unless they are displayed inline and not as a popover, they often create issues for keyboard users when they obscure other tabable elements, especially with reflow. Which oddly enough would be exacerbated if it is, in fact, so easy to automatically tab in and out of them. So, from an accessibility perspective, I consider non modal popups that stay open an edge case. Again that is not saying it's not being done but it is not intrinsically a very accessible pattern.

@theKashey
Copy link

@jquense - the problem with portals is not about catching focus/blur events from them, but about making them tabbable in the "right", and not "browser-defined" order.
It's like a focus trap around drop down, which would move focus to the distant(portal) node once needed, and providing some markup-level API to make these operations be discoverable by focusNext API. Ie - could not be solved by the imperative code.

@devongovett
Copy link
Author

I think we have different expectations of behavior for a focus trap.

Ah I see. The aria practices spec for modals says that focus should cycle within the modal, not go back to the browser chrome. Typically there are other keyboard shortcuts for that.

Focus trapping is consider bad behavior. When focus becomes trapped, it implies that the user is trapped.

Focus isolation better describes the practice of intentionally limiting focus to a container.
@theKashey
Copy link

It's fragile at best because you get an inconsistent behaviour where parts of the UI show focus for a frame, only for it to flicker back after you control the sentinel.

Not sure I follow. I didn't notice any issues with this approach.

What if you nest multiple FocusScopes and want the focus to propagate when leaving each FocusScope?

In this case, you have to manage sentinels as well, but usually, only one lock is active in a single point of time.

Ultimately, managing focus via tab is the only cross-platform applicable way of dealing with it

What about splitting Focus Management and Tab Management? One part emulates focus change, and could be reimplemented in user space(to support Gamepads?), while another one just controls it.

Copy link

@craigkovatch craigkovatch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This RFC makes me so, so happy. We really need something like this in React -- doing robust, correct focus management is currently SUCH A PAIN, requires a lot of un-reacty code, and is way less performant than direct DOM manipulation in a non-React component. It breaks everything that's nice about React, but it's also an absolute requirement.

Thank you for putting the work into thinking about this, for writing it up, and for publishing it for public comment. You're making the web a better place. My commentary below is a bit...volumous. Please consider that to a reflection of my excitement for the future success of this proposal and not anything negative 🎉

text/2019-focus-management.md Outdated Show resolved Hide resolved

The following terms will be used throughout this RFC.

- A **focusable** element is any DOM element which has a `tabIndex` property, along with a set of default elements such as `input`, `button`, etc.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

text/2019-focus-management.md Outdated Show resolved Hide resolved
text/2019-focus-management.md Outdated Show resolved Hide resolved
text/2019-focus-management.md Outdated Show resolved Hide resolved
focusNextTabStop(): void;

/* Focus the previous tabbable element after the currently focused one. */
focusPreviousTabStop(): void;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious what usages you imagine the focus*TabStop methods -- doesn't the browser-default behavior of Tab do this for us?

text/2019-focus-management.md Show resolved Hide resolved
text/2019-focus-management.md Outdated Show resolved Hide resolved
text/2019-focus-management.md Outdated Show resolved Hide resolved
text/2019-focus-management.md Outdated Show resolved Hide resolved
@devongovett
Copy link
Author

devongovett commented May 29, 2019

I have pushed an update to this RFC based on the feedback on the PR, and the POC in react-events. Thanks everyone for your thoughtful feedback! 🙏

Here is a summary of the changes:

  • Refer to focus "containment" everywhere instead of isolation or locking. The lock prop is replaced with a contain prop.
  • Make restoring focus opt-in, using the restoreFocus prop.
  • Add an autoFocus prop to focus the first item in a scope on mount.
  • Completely rewrite the "Implementation" section to remove the idea of using tabIndex to control the focus order, and instead just handle the Tab key in React. This is much simpler to implement and should be less controversial - see discussions above.
  • Remove roving tab index pattern from the scope of this RFC since it can be implemented fairly easily in user space by combining FocusScope, FocusManager, and some manual state management for tabIndex. In addition, the logic for what item to make tabbable often depends on other state such as what items are selected or disabled, so handling it in React doesn't make a whole lot of sense. The listbox example is updated to reflect how that would be implemented by users.

@devongovett
Copy link
Author

I made a pass at implementing the FocusManager API in addition to the already implemented FocusScope. facebook/react#15849

The API I ended up implementing is a little different than the one I proposed here (with a few more features), and I plan to update the RFC based on it. Feel free to leave feedback!

@webOS101
Copy link

We have a complete focus management implementation in React in https://github.com/enactjs/enact/tree/master/packages/spotlight that is already used on millions of devices. Was this implementation considered at all? I haven't had a chance to dig deeply into this proposal but it would be very interesting to at least share ideas and problems we've encountered in delivering this.

There is also work being done on a W3C spatial navigation proposal that is relevant: https://www.w3.org/TR/css-nav-1/

Copy link

@webOS101 webOS101 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are my first-pass comments on the RFC. Thank you for attempting to tackle this important issue. Focus management and spatial navigation are very important to accessible design and it would be great to have an API that would simplify its implementation within React.


# Motivation

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](https://www.w3.org/TR/wai-aria-practices-1.1/#keyboard) document for more information about focus management and keyboard interfaces.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be called 'spatial navigation' instead? Focus management, to me, is a broader concept that can also refer to things such as pointer/touch interaction.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have a different idea of what focus management is. This RFC is dealing with focus in the traditional browser sense, just utilizing the React tree order rather than the DOM order, and providing more declarative primitives for moving focus around in React rather than relying on imperative DOM APIs.

I think you're thinking of an even higher level concept (spacial navigation), which could be implemented on top of this to move focus around in response to actual events (e.g. arrow keys or gamepad). React won't be doing any of that out of the box, just providing the primitives so that other libraries can be built on top.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The discussion of keyboard interactions led me to question that. Also, the inclusion of tab-focus methods. It was not solely restricted to tracking focus with the blur/focus methods and did delve into controlling the focus within a FocusScope


### 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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it always re-focus the last selected item? This should be configurable to allow for scenarios where the most logical item to be refocused could differ (e.g. the selected item in a dropdown). Given this is configurable below, perhaps it's better to say 'could be restored' or to otherwise change this section to indicate this is optional/configurable behavior.

}
```

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. Users may expect the order to be input 1, input 2, input 3, as declared in the React tree. The current behavior is non-deterministic — it depends on the specific implementation of the Portal component (i.e. where it is placed in the DOM), which could change over time and cause the tab order to differ from what was intended. It also depends on the order portals are appended to the DOM rather than the order they are declared. These issues would be solved if the tab order in portals were based on the order in the React tree rather than the DOM tree. This would also match other existing portal behavior such as event bubbling where portals behave as if they are inline.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't take into account DOM ordering based on RTL/LTR, flexbox ordering or other CSS techniques for element relocation.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RTL/LTR is a layout order, not structural order. This follows the existing DOM behavior or following structural order, but of the React tree instead of the DOM tree. RTL/LTR still follow structural order but in the opposite visual direction. There isn't anything React needs to do to support that - it will happen by default in the browser when using a flipped layout (e.g. flexbox).

Copy link

@webOS101 webOS101 Jun 17, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you're absolutely correct. I hardly ever deal with tab focus since we completely override that behavior hardly ever use that behavior. 👍


If the `contain` prop is not passed to the `FocusScope`, or the scope is the implicit root `FocusScope`, then the user can tab in and out of the scope at will. Once focus is inside the scope, components can use the `FocusManager` API to programmatically move focus within the scope, for example in response to arrow keys or other interactions. This exact behavior is not provided by React, but APIs to perform these actions are available for component libraries to use. See below for details.

The `autoFocus` prop can also be passed to a `FocusScope`, which automatically focuses the first focusable element within that scope on mount.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How could I configure a default focus item instead of the first focusable? The first focusable element (in the DOM tree) may not be the logical item to focus.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you'd do that by putting an autoFocus prop directly on the element you wanted to focus, rather than on the scope.

<FocusScope>
  <input />
  <input autoFocus />
</FocusScope>

This is already supported by React, so nothing new would need to be implemented.


While handling the Tab key may not cover all possible cases across all browsers and platforms, it is the only reliable way to implement this behavior without relying on features like positive `tabIndex` (which can cause other accessibility problems) or additional "sentinel" DOM nodes (which can affect e.g. CSS selectors). If a platform implements focusing behavior that does not fire Tab key events, then the behavior would be exactly as it is today - following DOM order rather than React tree order. Additional support for these platforms could be added to React in the future as well if they are deemed important enough to support.

When the `restoreFocus` prop is enabled, the last focused element is stored by React when the `FocusScope` mounts. When the `FocusScope` unmounts, focus is restored to that element. If the element is no longer in the DOM, then focus is restored to the body (the default browser behavior).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you be able to deal with items that are not initially rendered on mount (e.g. VirtualList items). Is there some method that could be made available either through placeholders or callbacks that would allow the developer to cache and restore focus compatible with this system?


## FocusManager

In addition to defining the focus scopes in an application, components need an API to programmatically move focus around. The singleton `FocusManager` API exported by `react-dom` provides this interface.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a singleton disallows library authors from having access to custom management within components/sections. It also disallows custom components from specifying their own, local focus management. How could these situations be handled?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be moving to a context-based API. We've done some POC work here and implemented a useFocusManager hook to access the focus manager, instead of using a singleton. I'll update the RFC to reflect that.

@webOS101
Copy link

One other comment that occurred to me is that there does not appear to be anything in the spec to deal with focus set through pointer interaction or programmatic focus (el.focus()). Will the system tap into focus/blur events to update last focused item or will it only be able to work with focus set through tab or the focus manager API?

@devongovett
Copy link
Author

Yes, this will all be built on top of the standard focus implementation in browsers utilizing native focus and blur events, .focus() etc. It's just about providing declarative primitives in React to make dealing with these existing concepts simpler.

@webOS101
Copy link

Thanks for the responses. I'll look over the implementation you linked some more and see how it interacts with the implementation of focus management we use.

@Aliaksei-Martsinkevich
Copy link

Is it possible to know if the current FocusScope holding focus? I think it is might be handy when working with for example menu, when we want to keep it opened while it, or one of it's items is focused. I cannot think of an appropriate API though.

@sophieH29
Copy link

@devongovett thank you first of all on starting this! 👍 That's fantastic work!

This topic of focus management is very close to me as I am a contributor to open-source react library called Stardust and my main responsibility is to make sure that each component is delivered accessible.
In Stardust, we leverage Fabric UI's component for focus management called FocusZone and also FocusTrapZone for trapping focus. They have really nice concepts based on their lessons learned, maybe something can be borrowed for React Flare.

I went through your RFC and here some thoughts which are on top of my mind I want to share:

  • "handling the Tab key using a global keyboard event handler.  If the target of the event is within the FocusScope, the original event is canceled and the next element in the tab sequence computed from the React tree is focused programmatically" - I don't really like the idea of overriding native browser behavior. Also, all screen readers rely on it and that focus moves the natural DOM order. If FocusScope of the component will handle correctly tabindex, so in case of List one of the items will have set tabindex=0, the tabbing should work natively correctly. But I DO think that for focus trap functionality, we should handle TAB key down event to make sure user won't tab out from the scope.
  • "If the element is no longer in the DOM, then focus is restored to the body (the default browser behavior)" - restoreFocus is a great idea. Often it is needed to restore focus not one step back, but often previous active element is removed, I would consider having the history of some specific range of previously active elements. So we can return the user to the previously focused element more than 1 step back and do not allow the focus to move to the body. When some element disappears and the focus moves to the body - it is very tough for screen reader and keyboard users.
    One more thought about restoreFocus - take for example a popup. Sometimes it is needed to put the focus, not on the element which triggered that popup. User should be able to optionally specify which element to focus after popup dismiss
  • autoFocus - by default, as you specified, should be the first focusable element in the scope. But, again, give a possibility to optionally specify which element to focus when component mounts.
  • Base on Arrow key navigation example -
  1. "Initially, all <li> elements have tabIndex="0" to make them tabbable" - what is the reason to make all items in the list tabbable? By default, tabindex=0 is set to the 1st item of the list. Others -1. When navigate using ArrowUp/ArrowDown, tabindex=0 is set to last focusable item. When tabbing from the list and then tab back, the focus will go to the item with tabindex=0 (what you've mentioned already about roving index). If list re-renders, tabindex=0 will be restored back to the 1st item. Seems to be expected behavior.
  2. "focusNext" on the last element should move focus to the first element of the current scope - focus cycling should be set by default but also give a possibility to turn it off. If you are on the last item of the list and "focusNext" is triggered, the focus should still remain on the last one. One of the reasons, when using screen reader, item index is not always narrated (for e.g. "2 of 10"). A blind user won't know where the list ends.
  • would be great to integrate a possibility of defining if a focus comes from a keyboard or another device, for e.g. - mouse. Why? Focus ring should not be visible for mouse users, but for keyboard users. Currently, people use different 3rd party libraries to achieve that
  • What is the perfomance implication of having focus management? Has it been forecasted or measured somehow already?

Would love to hear your thoughts on that.
I genuinely believe that if build the focus management out of the box in React it will make life easier for thousands of engineers.

@krotovic
Copy link

krotovic commented Aug 4, 2019

I really like this idea and I think it could be implemented as some library that React will use rather then make it React-specific.

@dantman
Copy link

dantman commented Aug 5, 2019

@krotovic I feel mixed.

I partially agree with this proposal. I definitely agree in regards to things like the amount of refs needed to do something that is a standard pattern outside of React. I think the proposal hits the mark in regards to the ability to declaratively say "I want everything outside this component to have inert aria-hidden="true" on it". Additionally I find implementing most of the ARIA patterns to be a pain because most of them have special keyboard patterns which require refs, binding keyboard events, and then diving into key/keyCode. Even implementing roving tabIndex is a pain even when done mostly declaratively simply because it adds a lot of boilerplate code to implement a common pattern. For all of these it would be lovely to have a straightforward way to declaratively state our intent to follow a common pattern. And those are really React specific issues so it makes sense for them to be a react RFC.

But I do think the proposal goes a bit far when it tries to do stuff outside the standard toolkit, namely things that are difficult/impossible to get right even when you aren't using react.

@krotovic
Copy link

krotovic commented Aug 5, 2019

@dantman I may not express myself very clearly. I thought to extract the part that isn't necessarily React specific e.g. FocusManager to simple library that can be implemented by other UI libraries like React, Vue, Angular, etc. I'm not sure if it could help some of those libs but it may be considered.

@theKashey
Copy link

In this case, it should rely on DOM as a source of truth, and such libraries already exist.
React might need a special threatening due to portals (but it might just "open" fiber traverse api), and React Native, where no "DOM" exists (but again the problem could be solved via extra API exposed)

@sebmarkbage sebmarkbage mentioned this pull request Jan 27, 2020
@frastlin
Copy link

frastlin commented Feb 4, 2020

I would really like to see this in React. The majority of react component libraries have an extremely difficult time with focus management. This means there are 0 accessible dataGrids in React, and the debates in the React Router world has meant that almost every website with react does nothing around alerting screen reader users something happened.
As it currently stands, most react websites are on the bottom of the list in terms of user experience with screen readers. Adding this tool into the react developer's toolbox will make life significantly easier.
For example, the initial React tutorial would be a circumstance where a screen reader user would expect a dataGrid. The problem is that a dataGrid is so difficult with React, that a simple project like the tutorial becomes a major undertaking.
It's not just react that has this problem either, building a dataGrid is extremely difficult in any HTML framework.
But React is here to make the lives of developers easier, and focus management is currently defeating that purpose.
I would say that we should be focusing on Modals and dataGrids as the two extreme uses of this tool. Pretty much everything else is very easy. Once this is released, I expect the React tutorial to update to using this API.

@gaearon
Copy link
Member

gaearon commented Aug 18, 2021

Per #182, I'll comment that this is an area we're very interested in (in the longer term) but it's not currently on our near-term roadmap. We've had a few internal experiments in this area (here's a test file for the low-level APIs we've used). But we're unsatisfied with how overly generic the API we tried is, and that experiment probably doesn't have a future.

When we get to this problem space, we'll be sure to explore this thread for inspiration. If you've implemented something related in userland and it gave you some ideas, please don't hesitate to share it here too. I'll keep this open because we're not ready to review this.

@souporserious
Copy link

souporserious commented Aug 18, 2021

Thank you for the interest still in this @gaearon! It will be a huge help. For posterity, my latest attempt at solving this was in a custom hook that helps to keep track of ids called use-item-list. I also wrote a small post on hacking useMutableSource to make this work with concurrent mode, although, I haven't looked at it in a while so it may be a broken approach now. This library use-ref-list has an example using that technique with a codesandbox here.

@gaearon
Copy link
Member

gaearon commented Nov 14, 2021

This is also interesting: https://twitter.com/pirelenito/status/1459892048308408327

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

Successfully merging this pull request may close these issues.

None yet