+
+ >
+ );
+ 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]);
+ });
});