Skip to content

Commit c45fe88

Browse files
committed
fixup! ✨(frontend) add keyboard navigation for subdocs with focus activation
1 parent 528352b commit c45fe88

File tree

3 files changed

+82
-5
lines changed

3 files changed

+82
-5
lines changed

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

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Box, BoxButton, Icon, Text } from '@/components';
1212
import { useCunninghamTheme } from '@/cunningham';
1313
import { Doc, useTrans } from '@/features/docs/doc-management';
1414
import { useActionableMode } from '@/features/docs/doc-tree/hooks/useActionableMode';
15+
import { useLoadChildrenOnOpen } from '@/features/docs/doc-tree/hooks/useLoadChildrenOnOpen';
1516
import { useLeftPanelStore } from '@/features/left-panel';
1617
import { useResponsiveStore } from '@/stores';
1718

@@ -40,8 +41,8 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
4041
const { t } = useTranslation();
4142

4243
const [menuOpen, setMenuOpen] = useState(false);
43-
44-
const isActive = node.isFocused || menuOpen;
44+
const isSelectedNow = treeContext?.treeData.selectedNode?.id === doc.id;
45+
const isActive = node.isFocused || menuOpen || isSelectedNow;
4546

4647
const router = useRouter();
4748
const { togglePanel } = useLeftPanelStore();
@@ -81,13 +82,25 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
8182
}
8283
};
8384

84-
useKeyboardActivation(['Enter', ' '], isActive, handleActivate, true);
85+
useKeyboardActivation(
86+
['Enter', ' '],
87+
isActive && !menuOpen,
88+
handleActivate,
89+
true,
90+
);
91+
useLoadChildrenOnOpen(
92+
node.data.value.id,
93+
node.isOpen,
94+
treeContext?.treeData.handleLoadChildren,
95+
treeContext?.treeData.setChildren,
96+
(doc.children?.length ?? 0) > 0 || doc.childrenCount === 0,
97+
);
8598

8699
// prepare the text for the screen reader
87100
const docTitle = doc.title || untitledDocument;
88101
const hasChildren = (doc.children?.length || 0) > 0;
89102
const isExpanded = node.isOpen;
90-
const isSelected = treeContext?.treeData.selectedNode?.id === doc.id;
103+
const isSelected = isSelectedNow;
91104

92105
const ariaLabel = `${docTitle}${hasChildren ? `, ${isExpanded ? t('expanded') : t('collapsed')}` : ''}${isSelected ? `, ${t('selected')}` : ''}`;
93106

@@ -143,6 +156,15 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
143156
background: var(--c--theme--colors--greyscale-100);
144157
}
145158
}
159+
160+
/* Ensure actions are visible when hovering the whole item container */
161+
&:hover {
162+
.light-doc-item-actions {
163+
display: flex;
164+
opacity: 1;
165+
visibility: visible;
166+
}
167+
}
146168
`}
147169
>
148170
<TreeViewItem {...props} onClick={handleActivate}>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ export const DocTreeItemActions = ({
151151
};
152152

153153
useDropdownFocusManagement({
154-
isOpen: isOpen || false,
154+
isOpen: !!isOpen,
155155
docId: doc.id,
156156
actionsRef,
157157
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { useEffect, useRef } from 'react';
2+
3+
/**
4+
* Lazily loads children for a tree node the first time it is expanded.
5+
* Works for both mouse and keyboard expansions.
6+
*/
7+
export const useLoadChildrenOnOpen = <T>(
8+
nodeId: string,
9+
isOpen: boolean,
10+
handleLoadChildren?: (id: string) => Promise<T[]>,
11+
setChildren?: (id: string, children: T[]) => void,
12+
isAlreadyLoaded?: boolean,
13+
) => {
14+
const hasLoadedRef = useRef(false);
15+
16+
// Reset the local loaded flag when the node id changes
17+
useEffect(() => {
18+
hasLoadedRef.current = false;
19+
}, [nodeId]);
20+
21+
useEffect(() => {
22+
if (!isOpen) {
23+
return;
24+
}
25+
if (isAlreadyLoaded) {
26+
hasLoadedRef.current = true;
27+
return;
28+
}
29+
if (hasLoadedRef.current) {
30+
return;
31+
}
32+
if (!handleLoadChildren || !setChildren) {
33+
return;
34+
}
35+
36+
let isCancelled = false;
37+
// Mark as loading to prevent repeated fetches/renders that can cause flicker
38+
hasLoadedRef.current = true;
39+
void handleLoadChildren(nodeId)
40+
.then((children) => {
41+
if (isCancelled) {
42+
return;
43+
}
44+
setChildren(nodeId, children);
45+
})
46+
.catch(() => {
47+
// allow retry on next open
48+
hasLoadedRef.current = false;
49+
});
50+
51+
return () => {
52+
isCancelled = true;
53+
};
54+
}, [isOpen, nodeId, handleLoadChildren, setChildren, isAlreadyLoaded]);
55+
};

0 commit comments

Comments
 (0)