diff --git a/packages/react-aria-components/test/Table.test.js b/packages/react-aria-components/test/Table.test.js index 42829a5ae7a..0e6894ca173 100644 --- a/packages/react-aria-components/test/Table.test.js +++ b/packages/react-aria-components/test/Table.test.js @@ -423,6 +423,69 @@ describe('Table', () => { jest.clearAllMocks(); }); + it('should restore focus to the associated dialog trigger inside a cell', async () => { + let tree = render( + <> + + + + + Name + + Type + Date Modified + + + + Games + File folder + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + ); + await user.click(tree.getByRole('button', {name: 'Open 2'})); + let dialog = tree.getByRole('dialog'); + expect(document.activeElement).toBe(dialog); + await user.keyboard('{Escape}'); + act(() => jest.runAllTimers()); + expect(dialog).not.toBeInTheDocument(); + expect(document.activeElement).toBe(tree.getByRole('button', {name: 'Open 2'})); + }); + it('should render with default classes', () => { let {getByRole} = renderTable(); let tableTester = testUtilUser.createTester('Table', {root: getByRole('grid')}); diff --git a/packages/react-aria/src/grid/useGridCell.ts b/packages/react-aria/src/grid/useGridCell.ts index b998a245ab8..93cd5729ec0 100644 --- a/packages/react-aria/src/grid/useGridCell.ts +++ b/packages/react-aria/src/grid/useGridCell.ts @@ -106,20 +106,37 @@ export function useGridCell>( // focus to go to the item when the DOM node is reused for a different item in a virtualizer. let keyWhenFocused = useRef(null); + // Tracks the specific focusable child that was last focused within this cell. + let lastFocusedChild = useRef(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; } + let ownerDocument = getOwnerDocument(ref.current); + let shouldRestoreToLastFocused = + !activeElement || activeElement === ownerDocument.body || activeElement === ref.current; + if (shouldRestoreToLastFocused) { + 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) @@ -309,7 +326,7 @@ export function useGridCell>( // 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 @@ -318,6 +335,12 @@ export function useGridCell>( // 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. + + let target = getEventTarget(e) as FocusableElement; + if (ref.current && nodeContains(ref.current, target)) { + lastFocusedChild.current = target; + } + if (!isFocusVisible()) { state.selectionManager.setFocusedKey(node.key); } diff --git a/packages/react-aria/test/grid/useGrid.test.js b/packages/react-aria/test/grid/useGrid.test.js index 20b1915ef35..617f07b0482 100644 --- a/packages/react-aria/test/grid/useGrid.test.js +++ b/packages/react-aria/test/grid/useGrid.test.js @@ -157,4 +157,25 @@ describe('useGrid', () => { await user.keyboard('[ArrowLeft]'); expect(document.activeElement).toBe(tree.getAllByRole('gridcell')[0]); }); + + it('should restore focus to the child that was last focused within a cell, not the first child', async () => { + 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]); + }); });