Repository navigation
fix: preserve tree focus when items arrive - #10689
Conversation
| listProps, | ||
| // Keep the collection's focus handlers when empty so arriving items can receive keyboard focus. | ||
| // Make the grid tabbable unless the empty state contains a tabbable element. | ||
| state.collection.size === 0 ? {tabIndex: hasTabbableChild ? -1 : 0} : undefined, |
There was a problem hiding this comment.
I think this is ok, for others following the logic
listProps consists of:
data-collection: "react-aria3579033781-_r_2am_"
onBlur: (e)=> {…}
onFocus: (e)=> {…}
onKeyDown: (...args)=> {…}
onKeyDownCapture: (e)=> {…}
onKeyUp: (...args)=> {…}
onMouseDown: ƒ onMouseDown(e)
tabIndex: 0
So now all of those event handlers will be there all the time.
In addition, we'll always have the collection id, which I think we were missing and was a bug as well.
And finally, the tabIndex. It's the same logic as before, just looks different.
There was a problem hiding this comment.
One problem with this is for our renderEmptyState since that could have tabbable content, which now can't be fully tabbed through since useSelectableCollection will force the gridlist/tree to be a single tab stop unfortunately.
@minwookshin #3399 details some of the cases that prompted the original change, maybe we could change useSelectableCollection to no longer do that?
There was a problem hiding this comment.
That use case isn't supported in ListBox right now, not sure which one has the right of it though. ListBox might be limited by aria roles. If the change to useSelectableCollection fixes it for GridList, we'll need to double check on ListBox to make sure that matches expectations too.
Here's two tests to compare.
ListBox test
it('should support empty state', async () => {
let {getByRole} = render(
<ListBox
aria-label="Test"
renderEmptyState={() => (
<div>
<a href="/#">No results</a>
<a href="/#">Another link</a>
</div>
)}>
{[]}
</ListBox>
);
let listbox = getByRole('listbox');
expect(listbox).toHaveAttribute('data-empty', 'true');
await user.tab();
expect(document.activeElement).toBe(getByRole('link', {name: 'No results'}));
await user.tab();
expect(document.activeElement).toBe(getByRole('link', {name: 'Another link'}));
});
GridList test
it('can tab to all interactive elements in an empty state with focusable content', async () => {
let {getByRole} = render(
<GridList
aria-label="Test"
renderEmptyState={() => (
<div>
<a href="/#">No results</a>
<a href="/#">Another link</a>
</div>
)}>
{[]}
</GridList>
);
let gridList = document.querySelector('.react-aria-GridList');
expect(gridList).toHaveAttribute('data-empty', 'true');
await user.tab();
expect(document.activeElement).toBe(getByRole('link', {name: 'No results'}));
await user.tab();
expect(document.activeElement).toBe(getByRole('link', {name: 'Another link'}));
});
There was a problem hiding this comment.
Kept Tab and text selection working in empty GridList/Tree content using the existing collection options. Added regressions for both Tab directions and input editing, and verified that ListBox behavior is unchanged.
| const CONTAINER_HEIGHT = 300; | ||
| const items = Array.from({length: 50}, (_, i) => ({id: `item-${i}`, name: `Item ${i}`})); | ||
|
|
||
| it('navigates to asynchronously added rows after focusing an empty tree', async () => { |
There was a problem hiding this comment.
I think we can remove this one, browser tests are expensive and you showed that the unit tests can prove this.
Or did you add it for a particular reason?
There was a problem hiding this comment.
Removed this in 6074352; it duplicated the empty-to-populated keyboard path already covered by the Tree unit test. There wasn't a separate browser-specific case.
| }); | ||
|
|
||
| it.each([false, true])( | ||
| 'should preserve focus when items arrive after an empty list (blurred: %s)', |
There was a problem hiding this comment.
What's preserving focus? did you run into a bug that necessitated these conditions? or is there a code branch that this is covering?
There was a problem hiding this comment.
It'd be good if you answered these questions (all comments in review) in addition to making changes, otherwise it's hard to follow intent
|
Removed the duplicate browser test and split the focused and blurred flows into separate unit tests. The second case checks that items arriving after Tab don't move focus back into the list. |
snowystinger
left a comment
There was a problem hiding this comment.
Thanks, I think this looks good
Closes #10660
A focused empty Tree should remain keyboard-navigable when its rows arrive. Keep the collection's focus handlers attached while empty, while allowing normal Tab navigation and text selection in empty-state content. This keeps focus tracking intact without moving focus back after the user has left the collection; ListBox behavior is unchanged.
Added Tree and GridList regressions and a
PopulateFocusedEmptyTreestory that adds rows without moving focus. AI-assisted; repository guidance was followed and the diff was reviewed.✅ Pull Request Checklist:
Updated documentation (if it already exists for this component).No public API or documented behavior changes; added a reproduction story.📝 Test Instructions:
Open the new Tree story, Tab into the empty tree, press Enter to populate it, and use ArrowDown to navigate both rows. In a loading example, moving focus out before items arrive should leave focus outside the tree. Empty-state links should work in both Tab directions, and text inputs should retain their native select-all shortcut.
Local runner notes: SSR used
BROWSERSLIST_IGNORE_OLD_DATA=truefor an outdated data warning. Storybook needed a temporary PostCSS path-decoding workaround for a non-ASCII checkout path; it was restored after verification and is not part of this PR.🧢 Your Project:
Independent open-source contribution.