fix(useSelectableCollection): focus previous tab stop on Shift+Tab instead of collection root#10247
fix(useSelectableCollection): focus previous tab stop on Shift+Tab instead of collection root#10247coolassassin wants to merge 1 commit into
Conversation
… out of a collection Avoids unnecessary focus transitions and re-renders in Table when exiting via Shift+Tab from internal tab stops.
|
Thanks for the interest, as you can see, there's a bit of complexity with moving focus ourselves. It'd be better if we can let the browser handle the focus movement. This will help it work with things like shadow dom and iframes. The approach I'd look into would be to solve the re-renders by reducing the props or context that change for the cells. Did you try something like that and run into problems? Can you submit an issue with reproduction instructions? |
|
@snowystinger Did the issue with details: From my perspective, the main issue is that the table collection lives in React and works with React components. If there were an option to provide a table model to the table as a data structure, it would be possible to build a much more efficient solution for data grids. On the other hand, the current table solution updates the entire state on any change by design. If there were a way to update only the necessary parts of the state, it would be much more performant even with the current approach, but that would require a fairly significant change and refactoring. So, this PR is just a small fix for a local issue, trying to keep everything as is. |
|
I see, but it doesn't keep everything "as is". There are multiple cases this would now break as I pointed out previously. We should try turning on the React Compiler, that may reduce the number of re-renders we see without needing to do anything. Another thing we could try is, we know that during shift+tab, when we move focus to the collection root, we're also going to immediately lose it, so we could try to skip the focus ring updates there. Another approach could be to use useSyncExternalStore instead of the context, then only subscribe the row or cells that need to be updated. |
|
@snowystinger About the current solution, am I right that it is not what you as a team want to see in your code base? I mean, if I want to fix it, I have to find a better solution? |
|
@snowystinger It is less efficient (fix all rows that are not focused), but it has a significant impact as well |
No, I wouldn't expect it to be a silver bullet, but it might eliminate enough unstable things that it is "good enough". At the very least it'd get rid of some noise. Once the rest of the team is back from break I will ask. |
Summary
When Shift+Tab is pressed inside a collection with
allowsTabNavigation={false}(Table, ListBox, GridList, etc.),useSelectableCollectioncalledref.current.focus()on the collection root before the browser continued tabbing. That intermediate focus on the grid root is an unnecessary pivot — focus should move directly to the previous tab stop outside the collection.For RAC Table this caused extra focus transitions and downstream React re-renders (for example Row
useFocusRing({ within: true })when focus briefly lands on the grid root).The fix mirrors the forward-Tab branch: on Shift+Tab, when the previous external tab stop matches the collection boundary, we
preventDefaultandfocusWithoutScrollingthat element. Otherwise we fall back tocollectionRoot.focus()(nested collections, mobile submenu back button, etc.).Changed files:
packages/react-aria/src/selection/useSelectableCollection.tspackages/react-aria/test/selection/useSelectableCollection.test.jspackages/@adobe/react-spectrum/test/table/TableTests.jsGuards:
prev === prevFromRoot— direct exit only from the collection boundary as a single tab stopprevmust not belong to another[data-collection]outside the current root[role="dialog"]but outside the collection use the fallback pathstopPropagation()(ComboBox dev-mode)✅ Pull Request Checklist:
📝 Test Instructions:
Automated
The new test uses
fireEvent.keyDown(notuser.tab({ shift: true })) so it exercisesuseSelectableCollection's Tab handler — same caveat as GridList.test.js.Manual (Table / Storybook)
Regression
🧢 Your Project:
dxFeed (Devexperts) — internal Table built on react-aria-components (
Table,div-grid,focusMode="cell").