-
Notifications
You must be signed in to change notification settings - Fork 47.1k
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
[Flare] Rethinking Focus #16009
Comments
This caught my eye but only because I'm doing keyboard navigation on lists wrong currently. Anyone thinking this is bad for long lists which are navigateable by arrow keys: The whole list stays focused while listitems are marked with The rest looks fine. Should this be an RFC? |
@eps1lon That’s a good point. How do you envisage focus targets working in your use case? Note also: controlling focus directly should work in tandem with aria properties.
This is for React Flare which is still all experimental right now (and internal only). |
I don't think I would use these components for a list that is navigateable with the keyboard. Unless the examples on the WAI-ARIA authoring practices are inaccessible it seems like aria-activedescendant works good enough. At least NVDA works reasonably well and doesn't require actual focus on the selected options. It seems like "just" adding tabIndex=-1 to the interactive widgets is good enough. The problem only exists if we want to implement roving tabIndex. FocusScope seems only interesting for Modals as far as I can tell. But I've only glanced at the current implementation progress and FocusScope rfc so I'm probably missing more use cases. |
@eps1lon When you use the keyboard arrows to move up and down the list, you'd move current focus between accessibility components. The additional benefit is this will work in browsers that don't support To clarify though – I don't think you'd be using |
We have an interesting use case that this could solve if we allow users to bring their custom Would this be something that this proposal can help us with? |
This would definitely work in those cases. Did you mean |
I did mean We could use |
It sounds like you also wanted to enforce that focusable means tabbable which elements of a dropdown shouldn't be. This was my concern. |
I've removed the ideas around
I've taken your feedback and revised my original post! :) Notably, instead of using const FocusableInput = ReactDOM.createAccessibleComponent((props, focusable) => {
return <input tabIndex={focusable ? 0 : -1} {...props} />;
});
// now it's focusable
<FocusScope>
<FocusableInput
type="text"
placeholder="Enter your username"
focusable={true}
/>
</FocusScope> |
Maybe I'm thinking to close to how the DOM works but a listitem shouldn't actually be focused i.e. document.activeElement |
@eps1lon Yeah, I think it's too close to thinking in relation to the DOM. This new event system allows us to forget about how the DOM and browsers work in regards to much of the complications today. We can set a new set of standards that are more applicable to how applications in React are used and interacted with today – just like React did for UIs. With Flare, internally we've advised that no one uses pseudo selectors like With Flare, users wouldn't be using |
@trueadm I guess @eps1lon's concern might be valid. I will try to clarify and you or him can correct me if I am wrong. In the case of dropdown menus the list never gets focus. Instead the toggle button is always the active element. When using the screen reader you can use the up/down arrow keys to navigate the list but (important) focus stays on the toggle button (basically you only control the screen reader cursor with up/down). This is to allow the user to tab away to the next item and close the menu on blur (or at least this is how many implement it). |
@giuseppeg I totally get that. I was saying that the support for that aria property is limited between browsers though and function DropdownList() {
const [showDropdown, updateShowDropdown] = useState(false);
useLayoutEffect(() => {
if (showDropdown) {
focusManager.focusId('dropdown');
}
});
<Focus onFocus={() => updateShowDropdown(true)}>
<DropdownButton />
showDropdown && (
<FocusScope focusId="dropdown" onKeyPress={key => {
if (key === 'Tab') {
// Move to the next focusable node outside this focus scope
focusManager.focusNext('dropdown');
} else if (key === 'ArrowDown') {
focusManager.focusNext();
} else if (key === 'ArrowUp') {
focusManager.focusPrevious();
}
}}>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</FocusScope>
)
</Focus>
} That's a rough example of what I meant. :) |
I would guess that you implement it in a way that TAB exits the lists focus scope. Aside: the focus is on the list not the button that opened it. |
This seems more similar to the original RFC I wrote than the current implementation, and in fact quite similar in terms of the suggested implementation strategy from the RFC. In terms of perf, the RFC also proposed a separate focus tree similar to what you're describing. Elements are added to the nearest FocusScope as they are mounted, rather than collecting them every time the tab key is pressed. It also goes back to the singleton focus manager API that I originally proposed, which I think might be easier to use and probably faster than the I had also considered some kind of wrapper to mark focusable nodes, similar to your I originally decided not to go that way due to interoperability concerns. For us, we'd use So in summary, this seems pretty good to me. I'm not sure about the name |
@eps1lon yeah I have bugs in that example above, but hopefully it conveys what I was trying to explain better! |
I was on mobile and didn't see the implementation. This was addressed at @giuseppeg. But this looks really nice. Especially decoupling it from element ids is pretty nice. No need for some custom id generation logic since this will all be contained within a sub-tree. |
This sounds awesome; really cool to read through the ideas here. One thing I want to reiterate is be sure to test often in NVDA/JAWS and Voiceover–ARIA roles and accessible names can make a big impact on whether something will be announced when focused. We're working on some accessible client-side routing improvements in Gatsby.js and my colleague shared this proposal with me–it seems more useful for modals and components than route changes per se, but I'll be following the progress. Let me know if you need any more testing done, I'm happy to try some things out and provide feedback. |
Targeting
I'm not sure I understand the selection algorithm you're proposing:
If my understanding is correct, the "choose from your children" feels like it could be dangerous in the event that any of your children rerender and you need to retain DOM synchronicity. If my understanding is incorrect, I'd love to see more explanation on this so I can better understand your proposal. OrderingTab order is defined by RTLI'm pretty sure that this is portion of the API is unnecessary. I believe that form elements are by default focused in DOM order (depth-first, in-order), regardless of the RTL/LTR state. Even if this does end up as something that the The only place where I know that it might matter is when clicking into a non-focusable area and deciding which node should be focused next. In the DOM world I'm pretty sure that this still doesn't require knowledge of LTR/RTL as you can simply track the DOM node. Multiple RootsIn the event of multiple React roots on a page, what should happen between competing |
I think (this issue hasn't been updated since), a better solution is to use handles rather than IDs in general. Then we avoid collisions with ids and can avoid all this complicated lookup logic.
Last time I checked, RTL did affect focus behaviour for screen readers. With React Flare, we wouldn't use the browser focus system, we use our own internal mechanism (required for Suspense and Portals). This isn't that big of a concern though, we can drop this from any API if we don't deem it necessary. We don't track DOM nodes though, we track fibers, which have a relative relationship to DOM nodes for host component fibers.
We haven't really discussed multiple roots at this time, but I don't see any real issues here. We control focus with the new React Flare system that is independent from the current React event system and also independent from the browser focus system. We can ensure events are properly acted on for their current roots by using the fiber tree to ensure consistency. |
Hi @trueadm and team, kudos to the work you're doing ❤️ Focus management has been a pain to fully make components accessible. Based on your example const FocusableInput = ReactDOM.createAccessibleComponent((props, focusable) => {
return <input tabIndex={focusable ? 0 : -1} {...props} />;
}); I wouldn't call it const FocusableInput = ReactDOM.createFocusableComponent((props, tabbable) => {
return <input tabIndex={tabbable ? 0 : -1} {...props} />;
}); |
@sophieH29 The |
I think this discussion need to delve into the notion of screen reader interaction modes. Nobody has mentioned this yet, but screen readers have two radically different ways of 'focusing' content, depending on whether they are browsing (headings, paragraphs, lists etc.) using a 'virtual cursor', or alternatively 'interacting' (form controls, hyperlinks and other GUI widgets) - which uses the 'tab order' that is perhaps more familiar to those who interact with the web via keyboard. Focus management which does not take this distinction into account is doomed to failure. You 'focus' the virtual cursor on non-interactive elements (such as headings) using a keyboard shortcut (e.g. H for next heading, I for next list item), and only then do they get announced. When you do a 'true' reload, the screen reader will announce the page title and typically start reading the first text nodes - especially those found in H1. When you re-render a single page app, none of this happens by itself. There is no way to programatically set the 'virtual cursor', except via the hack of focusing an element with tabindex set to -1. This is where accessibility implementations collide with the single-page-app paradigm. |
Looking at this from a react-native point of view this api would make a lot of sense for keyboard navigation/tv remote support. The proposed api has an |
We're no longer exploring the ideas I mentioned in this issue, so closing it now. |
@trueadm is any other solution being explored, regarding focus management? |
@bpierre The focus management stuff was pulled out of React Flare earlier on and the work ended up here (although this is old now, the latest code is only available internally at FB). https://github.com/facebook/react/tree/master/packages/react-interactions/accessibility/src |
@trueadm Thanks! |
@trueadm link is dead :( Or |
Link is dead, we removed that work. You can find it still in history for the react-interactions directory :) |
The refined github extension adds permalinks to any non-permalink github links. |
I think we need to rethink how focus works in React. React Flare is the perfection opportunity to allow us to do this, so here are some of my thoughts. None of these ideas wouldn't be possible if it weren't for the great ideas from @sebmarkbage, @devongovett and @necolas have had. Furthermore, the discussions in #16000, #15848 and #15849 got me thinking on a better system.
Focus is a mess on the DOM, so let's not use the DOM
Focusing on the DOM is a mess today. We couple ideas around ideas around things like
tabIndex
and whether a specific browser treats something as focusable. This is very much a hard-coded disaster where no one really agrees on a good formula for success. Not to mention, that this just doesn't translate well for a declarative UI. How does one tab to a specific node that isn't focusable? How does one use keyboard arrows to navigate a table using keyboard arrows?Then there's implementation. Without relying on an attribute on an element or a
ref
, it's very hard to say: "Hey look, let's focus to this node, given this criteria". Not to mention the performance overhead of doing this: querying or "collecting" focusable elements is an expensive O(n) task, which doesn't scale for large applications well. I noticed that wrapping the an internal large app with<FocusScope>
and then collecting all focusable nodes took over 850ms on Android using Chrome. Querying the DOM nodes took even longer.Lastly, we can't use the DOM with React Native and the story for handling focus with React Flare is important. If we instead had a React system for handling focus, then both the web and RN would be consistent and performant.
Accessible components
We already have the
<Focus>
and<FocusScope>
event components. We could extend on React Flare and introduce a way of layering accessibility logic on to host components. In this I introduce a new API calledcreateAccessibleComponent
, but really it could be anything – ignore the naming! This is purely hypothetical discussion for now.If you don't use a
FocusScope
, then the normal DOM behaviour will continue to work as expected.FocusScope
will only care about these new types of accessible component.The focus manager should be encapsulated and relative to
FocusScope
In order for focus management to be powerful, it needs to be baked into React. Event responders like
FocusScope
can let the manager know what scope it should be interacting with given a particular<Focus>
that focuses occur in.FocusScope
will also fully override the browser tabbing behaviour (like it does now) to ensure tabbing works as expected:Focusing by
focusId
will propagate until anfocusId
is found. So this would matter for cases such:If
focusManager.focusById('focus-me);
was used on the innerFocusScope
, it would focus the inner button. If used on the outerFocusScope
, it would focus the outer button. If the outerFocusScope
didn't have an id that matched, then it would propagate the lookup to the innerFocusScope
.Doing this, it makes it possible to apply keyboard navigation:
Furthermore,
<FocusScope>
s can also havefocusId
s that allows you to move focus to a specific scope. That particular event component can then act upon receiving focus<FocusScope onFocus={...}>
.It can simplify
<Focus>
Before, focus would only be of the direct child of the
<Focus>
component. This made it somewhat problematic when you wanted to find the focusable element that was not a direct child. Focus no longer needs to be coupled with "bubbling up" through the DOM, but rather it bubbles from accessible component to event components. So doing this, will still result in the nearest focusable child being passed to theFocus
:This can be fast too
In terms of performance, we can actually fast-path how this all works because we're no longer using the DOM, but event components within the Flare event system. We'd have optimized data collections that ensure that the least possible time is taken traversing focusable elements by leveraging a separate internal data structure that is separate from the Fiber and DOM structures. The cost is that this will take additional memory and time to construct when a focus scope gets mounted/unmounted. Given this shouldn't be a rapid event, it's worth the trade-off.
Also, given we're not wrapping
FocusScope
with a context provider (as mentioned in the FocusManager PR), which should get improved performance from not needing to do context lookups and traversals.Focus
andFocusScope
,focusManager
Given that they now share underlying implementation details, they all should come from the same module. So going forward, it makes sense to import them all form
react-events/focus
.The nice benefit from this is that this actually fixes a bunch of issues with the current implementation, where we can't use
FocusScope
as a hooked event component. With the changes outlined in this issue, it should allow for them to be used via theuseEvent
hook.We can build in great dev tooling around the focus system
We can build in great support for debugging in React Dev Tools when working with focus and
this will help improve accessibility within apps that use
<Focus>
,<FocusScope>
andfocusManager
. Plus it would support any future APIs that add accessibility benefits to components.The text was updated successfully, but these errors were encountered: