️(frontend) fix doc tree keyboard navigation regressions

Shift+Tab from sub-doc returns focus to root item
This commit is contained in:
Cyril
2026-03-12 17:10:09 +01:00
parent 1016b1c25d
commit a2ae41296d
5 changed files with 66 additions and 4 deletions
+1
View File
@@ -22,6 +22,7 @@ and this project adheres to
- 🐛(backend) create a link_trace record for on-boarding documents
- 🐛(backend) manage race condition when creating sandbox document
- ♿️(frontend) improve doc tree keyboard navigation #1981
## [v4.7.0] - 2026-03-09
@@ -271,6 +271,40 @@ test.describe('Doc Tree', () => {
await expect(rootMoreOptionsButton).toBeFocused();
});
test('Shift+Tab from resize handle returns focus to selected sub-doc', async ({
page,
browserName,
}) => {
const [docParent] = await createDoc(
page,
'doc-tree-shift-tab',
browserName,
1,
);
await verifyDocName(page, docParent);
const { name: docChild } = await createRootSubPage(
page,
browserName,
'doc-tree-shift-tab-child',
);
const selectedSubDoc = await getTreeRow(page, docChild);
await expect(selectedSubDoc).toHaveAttribute('aria-selected', 'true');
const resizeHandle = page.locator('[data-panel-resize-handle-id]').first();
await expect(resizeHandle).toBeVisible();
await selectedSubDoc.focus();
await expect(selectedSubDoc).toBeFocused();
await page.keyboard.press('Tab');
await expect(resizeHandle).toBeFocused();
await page.keyboard.press('Shift+Tab');
await expect(selectedSubDoc).toBeFocused();
});
test('it updates the child icon from the tree', async ({
page,
browserName,
@@ -159,7 +159,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
return (
<Box
className="--docs--doc-title"
className={CLASS_DOC_TITLE}
$direction="row"
$align="center"
$gap="4px"
@@ -186,6 +186,7 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
docId={doc.id}
title={doc.title}
buttonProps={{
tabIndex: -1,
$css: css`
&:focus-visible {
outline: 2px solid var(--c--globals--colors--brand-500);
@@ -220,6 +221,7 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
e.stopPropagation();
handleActivate();
}}
tabIndex={-1}
$width="100%"
$direction="row"
$gap={spacingsTokens['xs']}
@@ -19,6 +19,7 @@ import {
useTrans,
} from '@/docs/doc-management';
import { CLASS_DOC_TITLE } from '../../doc-header';
import { KEY_DOC_TREE, useDocTree } from '../api/useDocTree';
import { findIndexInTree } from '../utils';
@@ -120,11 +121,21 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
selectRoot();
navigateToRoot();
if (currentDoc.id === treeContext?.root?.id) {
document.querySelector<HTMLElement>(`.${CLASS_DOC_TITLE}`)?.focus();
} else {
selectRoot();
navigateToRoot();
}
}
},
[selectRoot, navigateToRoot, rootActionsOpen],
[
selectRoot,
navigateToRoot,
rootActionsOpen,
currentDoc.id,
treeContext?.root?.id,
],
);
// Handle menu open/close for root item - mirrors DocSubPageItem behavior
@@ -142,6 +153,13 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
}, []);
const handleRowKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Tab' && e.shiftKey) {
e.preventDefault();
e.stopPropagation();
rootItemRef.current?.focus();
return;
}
if (e.key !== 'Enter') {
return;
}
@@ -157,6 +175,13 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
return;
}
const treeItem = e.currentTarget.querySelector('[role="treeitem"]');
if (treeItem?.getAttribute('aria-selected') === 'true') {
e.preventDefault();
document.querySelector<HTMLElement>(`.${CLASS_DOC_TITLE}`)?.focus();
return;
}
e.currentTarget
.querySelector<HTMLDivElement>('.c__tree-view--node')
?.click();