Skip to content

Commit a2ae412

Browse files
committed
♿️(frontend) fix doc tree keyboard navigation regressions
Shift+Tab from sub-doc returns focus to root item
1 parent 1016b1c commit a2ae412

File tree

5 files changed

+66
-4
lines changed

5 files changed

+66
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ and this project adheres to
2222

2323
- 🐛(backend) create a link_trace record for on-boarding documents
2424
- 🐛(backend) manage race condition when creating sandbox document
25+
- ♿️(frontend) improve doc tree keyboard navigation #1981
2526

2627
## [v4.7.0] - 2026-03-09
2728

src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,40 @@ test.describe('Doc Tree', () => {
271271
await expect(rootMoreOptionsButton).toBeFocused();
272272
});
273273

274+
test('Shift+Tab from resize handle returns focus to selected sub-doc', async ({
275+
page,
276+
browserName,
277+
}) => {
278+
const [docParent] = await createDoc(
279+
page,
280+
'doc-tree-shift-tab',
281+
browserName,
282+
1,
283+
);
284+
await verifyDocName(page, docParent);
285+
286+
const { name: docChild } = await createRootSubPage(
287+
page,
288+
browserName,
289+
'doc-tree-shift-tab-child',
290+
);
291+
292+
const selectedSubDoc = await getTreeRow(page, docChild);
293+
await expect(selectedSubDoc).toHaveAttribute('aria-selected', 'true');
294+
295+
const resizeHandle = page.locator('[data-panel-resize-handle-id]').first();
296+
await expect(resizeHandle).toBeVisible();
297+
298+
await selectedSubDoc.focus();
299+
await expect(selectedSubDoc).toBeFocused();
300+
301+
await page.keyboard.press('Tab');
302+
await expect(resizeHandle).toBeFocused();
303+
304+
await page.keyboard.press('Shift+Tab');
305+
await expect(selectedSubDoc).toBeFocused();
306+
});
307+
274308
test('it updates the child icon from the tree', async ({
275309
page,
276310
browserName,

src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
159159

160160
return (
161161
<Box
162-
className="--docs--doc-title"
162+
className={CLASS_DOC_TITLE}
163163
$direction="row"
164164
$align="center"
165165
$gap="4px"

src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
186186
docId={doc.id}
187187
title={doc.title}
188188
buttonProps={{
189+
tabIndex: -1,
189190
$css: css`
190191
&:focus-visible {
191192
outline: 2px solid var(--c--globals--colors--brand-500);
@@ -220,6 +221,7 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
220221
e.stopPropagation();
221222
handleActivate();
222223
}}
224+
tabIndex={-1}
223225
$width="100%"
224226
$direction="row"
225227
$gap={spacingsTokens['xs']}

src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTree.tsx

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
useTrans,
2020
} from '@/docs/doc-management';
2121

22+
import { CLASS_DOC_TITLE } from '../../doc-header';
2223
import { KEY_DOC_TREE, useDocTree } from '../api/useDocTree';
2324
import { findIndexInTree } from '../utils';
2425

@@ -120,11 +121,21 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
120121

121122
if (e.key === 'Enter' || e.key === ' ') {
122123
e.preventDefault();
123-
selectRoot();
124-
navigateToRoot();
124+
if (currentDoc.id === treeContext?.root?.id) {
125+
document.querySelector<HTMLElement>(`.${CLASS_DOC_TITLE}`)?.focus();
126+
} else {
127+
selectRoot();
128+
navigateToRoot();
129+
}
125130
}
126131
},
127-
[selectRoot, navigateToRoot, rootActionsOpen],
132+
[
133+
selectRoot,
134+
navigateToRoot,
135+
rootActionsOpen,
136+
currentDoc.id,
137+
treeContext?.root?.id,
138+
],
128139
);
129140

130141
// Handle menu open/close for root item - mirrors DocSubPageItem behavior
@@ -142,6 +153,13 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
142153
}, []);
143154

144155
const handleRowKeyDown = useCallback((e: React.KeyboardEvent) => {
156+
if (e.key === 'Tab' && e.shiftKey) {
157+
e.preventDefault();
158+
e.stopPropagation();
159+
rootItemRef.current?.focus();
160+
return;
161+
}
162+
145163
if (e.key !== 'Enter') {
146164
return;
147165
}
@@ -157,6 +175,13 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
157175
return;
158176
}
159177

178+
const treeItem = e.currentTarget.querySelector('[role="treeitem"]');
179+
if (treeItem?.getAttribute('aria-selected') === 'true') {
180+
e.preventDefault();
181+
document.querySelector<HTMLElement>(`.${CLASS_DOC_TITLE}`)?.focus();
182+
return;
183+
}
184+
160185
e.currentTarget
161186
.querySelector<HTMLDivElement>('.c__tree-view--node')
162187
?.click();

0 commit comments

Comments
 (0)