♿️(frontend) fix doc tree keyboard navigation regressions
Shift+Tab from sub-doc returns focus to root item
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user