Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 18 additions & 13 deletions packages/react-aria/src/grid/useGridCell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,28 +254,33 @@ 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;

// FIX: If focus is moving directly to a specific inner child element
// (like our restored Open Dialog button), update the selection state
// and exit early. Do not call focus(), which resets to the first child.

// FIX: If focus is moving directly to a specific inner child element
// (like our restored Open Dialog button), update the selection state
// and exit early. Do not call focus(), which resets to the first child.
if (getEventTarget(e) !== ref.current) {
// useSelectableItem only handles setting the focused key when
// the focused element is the gridcell itself. We also want to
// set the focused key when a child element receives focus.
// 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.
if (!isFocusVisible()) {
state.selectionManager.setFocusedKey(node.key);
}
return;
}

// If the cell itself is focused, wait a frame so that focus finishes propagatating
// up to the tree, and move focus to a focusable child if possible.
requestAnimationFrame(() => {
if (focusMode === 'child' && getActiveElement() === ref.current) {
focus();
if (focusMode === 'child') {
// Safeguard: Check if the browser's active element has already shifted
// into a nested child node during this event loop tick before forcing a reset.
let activeElement = getActiveElement();
if (ref.current && isFocusWithin(ref.current) && activeElement !== ref.current) {
return;
}
});

focus();
}
};

let gridCellProps: DOMAttributes = mergeProps(itemProps, {
Expand Down
32 changes: 32 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,36 @@ describe('useGrid', () => {
await user.keyboard('[ArrowLeft]');
expect(document.activeElement).toBe(tree.getAllByRole('gridcell')[0]);
});

it('should retain focus on a specific child element if focus is restored to it', async () => {
let tree = renderGrid({gridFocusMode: 'cell', cellFocusMode: 'child'});
let switches = tree.getAllByRole('switch');
let cells = tree.getAllByRole('gridcell');

// 1. Initially move focus onto the grid via tab flow (focuses Switch 1)
await user.tab();
expect(document.activeElement).toBe(switches[0]);

// 2. Simulate focus returning directly to the second target element (Switch 2)
// exactly how the focus-restoration logic inside an overlay does it.
act(() => {
switches[1].focus();
});
expect(document.activeElement).toBe(switches[1]);

// 3. Fire a focus event directly on the gridcell container to trigger your
// onFocus handler in useGridCell.ts and simulate event bubbling
act(() => {
cells[0].dispatchEvent(new FocusEvent('focus', {bubbles: true}));
});

// 4. Force Jest's timer and requestAnimationFrame microtask cycles to execute completely
act(() => {
jest.runAllTimers();
});

// 5. FINAL ASSERTION: Focus should accurately stay locked onto Switch 2
// instead of resetting to the initial index element Switch 1!
expect(document.activeElement).toBe(switches[1]);
});
});