Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
11a8867
fix: prevent useGridCell from overriding child focus restoration loops
jsmitrah Jun 18, 2026
7e8bcfe
fix: resolve lint and CI test compilation errors
jsmitrah Jun 18, 2026
adbba17
changed the type for focus event.
jsmitrah Jun 19, 2026
bce1556
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Jun 26, 2026
24667bf
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Jun 29, 2026
e9c5635
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Jul 8, 2026
cf9fb80
fix: improve button interaction handling.
jsmitrah Jul 10, 2026
6a6f158
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Jul 10, 2026
70b8a96
chore: clear remote build cache
jsmitrah Jul 10, 2026
04dda2d
Merge branch 'bug-fix/8686/table-cell-incorrectly-returns-focus-to-bu…
jsmitrah Jul 10, 2026
2bce577
chore: clear remote build cache
jsmitrah Jul 10, 2026
ce7ac24
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Jul 13, 2026
480b06f
cleaned the test file.
jsmitrah Jul 15, 2026
0cf561f
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Jul 15, 2026
4228eb9
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Jul 16, 2026
d5fe949
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Jul 17, 2026
eb1152a
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Jul 17, 2026
11f3ad3
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Jul 22, 2026
4bb3a9d
modified the fix without removing the RAF.
jsmitrah Jul 23, 2026
75b2056
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Jul 23, 2026
983bad4
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Jul 24, 2026
e3b8dc3
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Jul 27, 2026
e6fac10
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Jul 28, 2026
8e6faf0
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Jul 29, 2026
b04e589
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Jul 31, 2026
e113f83
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Aug 3, 2026
4328795
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Aug 10, 2026
3d00705
Worked on PR comments.
jsmitrah Aug 11, 2026
11857eb
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Aug 11, 2026
801acd2
Reverted the useGrid.test.js file.
jsmitrah Aug 12, 2026
7b0b5e1
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Aug 12, 2026
49ded82
resolved lint
jsmitrah Aug 12, 2026
2861c45
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Aug 13, 2026
4f8f75e
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Aug 14, 2026
566dcd8
Trigger CircleCI build
jsmitrah Aug 14, 2026
ed78d21
Revert accidental filemode change on useGrid.test.js
jsmitrah Aug 17, 2026
c7a59d8
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Aug 17, 2026
c8adffe
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Aug 18, 2026
5d03aa1
Merge branch 'main' into bug-fix/8686/table-cell-incorrectly-returns-…
jsmitrah Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 40 additions & 5 deletions packages/react-aria/src/grid/useGridCell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,20 +106,42 @@ export function useGridCell<T, C extends GridCollection<T>>(
// focus to go to the item when the DOM node is reused for a different item in a virtualizer.
let keyWhenFocused = useRef<Key | null>(null);

// Tracks the specific focusable child that was last focused within this cell.
let lastFocusedChild = useRef<FocusableElement | null>(null);

// Handles focusing the cell. If there is a focusable child,
// it is focused, otherwise the cell itself is focused.
let focus = () => {
if (ref.current) {
let treeWalker = getFocusableTreeWalker(ref.current);
if (focusMode === 'child') {
let activeElement = getActiveElement(getOwnerDocument(ref.current));

// If focus is already on a focusable child within the cell, early return so we don't shift focus
if (
isFocusWithin(ref.current) &&
ref.current !== getActiveElement(getOwnerDocument(ref.current))
) {
if (isFocusWithin(ref.current) && ref.current !== activeElement) {
return;
}

// Focus counts as "disrupted" (as opposed to a fresh keyboard entry from a
// sibling row/cell) when it's on the cell itself or fell back to body/nothing,
// e.g. because an overlay closed and removed the element that had focus. In
// that case, restore the child that was last focused instead of defaulting to
// childFocusStrategy's first/last child.
Comment thread
snowystinger marked this conversation as resolved.
Outdated
let ownerDocument = getOwnerDocument(ref.current);
let isDisrupted =
Comment thread
snowystinger marked this conversation as resolved.
Outdated
ref.current === activeElement || !activeElement || activeElement === ownerDocument.body;
if (isDisrupted) {
let lastChild = lastFocusedChild.current;
if (
lastChild &&
keyWhenFocused.current === node.key &&
nodeContains(ref.current, lastChild)
) {
focusSafely(lastChild);
return;
}
}

let focusable =
state.selectionManager.childFocusStrategy === 'last'
? last(treeWalker)
Expand Down Expand Up @@ -309,7 +331,7 @@ export function useGridCell<T, C extends GridCollection<T>>(

// Grid cells can have focusable elements inside them. In this case, focus should
// be marshalled to that element rather than focusing the cell itself.
let onFocus = e => {
let onFocus = (e: FocusEvent) => {
keyWhenFocused.current = node.key;
if (getEventTarget(e) !== ref.current) {
// useSelectableItem only handles setting the focused key when
Expand All @@ -318,6 +340,19 @@ export function useGridCell<T, C extends GridCollection<T>>(
// If focus is currently visible (e.g. the user is navigating with the keyboard),
// then skip this. We want to restore focus to the previously focused row/cell
// in that case since the table should act like a single tab stop.

// Remember which child was focused so that if something later forces focus
// back to this cell (e.g. a closing overlay), focus() can restore it directly
// instead of falling back to the first/last focusable child. Only do this for
// actual DOM descendants of the cell -- portalled content (e.g. a dialog opened
// from within the cell) can still reach this handler because React dispatches
// events along the component tree rather than the DOM tree for portals, even
// though it isn't really inside this cell in the DOM.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not sure what the portalled comment is about? the line below nodeContains(ref.current, target) is a guard for portals

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, the nodeContains(ref.current, target) check prevents that. It ensures we remember only elements that belong to this cell, so focus is restored to the correct button when the dialog closes.

Comment thread
snowystinger marked this conversation as resolved.
Outdated
let target = getEventTarget(e) as FocusableElement;
if (ref.current && nodeContains(ref.current, target)) {
lastFocusedChild.current = target;
}

if (!isFocusVisible()) {
state.selectionManager.setFocusedKey(node.key);
}
Expand Down
21 changes: 21 additions & 0 deletions packages/react-aria/test/grid/useGrid.test.js
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,25 @@ describe('useGrid', () => {
await user.keyboard('[ArrowLeft]');
expect(document.activeElement).toBe(tree.getAllByRole('gridcell')[0]);
});

Comment thread
snowystinger marked this conversation as resolved.
it('should restore focus to the child that was last focused within a cell, not the first child', async () => {
Comment thread
snowystinger marked this conversation as resolved.
let tree = renderGrid({gridFocusMode: 'cell', cellFocusMode: 'child'});
let switches = tree.getAllByRole('switch');
let cells = tree.getAllByRole('gridcell');

await user.tab();
expect(document.activeElement).toBe(switches[0]);

await user.keyboard('[ArrowRight]');
expect(document.activeElement).toBe(switches[1]);

act(() => {
cells[0].focus();
});
act(() => {
jest.runAllTimers();
});

expect(document.activeElement).toBe(switches[1]);
});
});