Skip to content

Commit 2746c9e

Browse files
committed
✨(frontend) enable enter key to open documents and subdocuments
added keyboard support to open docs and subdocs using the enter key Signed-off-by: Cyril <[email protected]>
1 parent 9f9fae9 commit 2746c9e

File tree

8 files changed

+285
-93
lines changed

8 files changed

+285
-93
lines changed

CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@ and this project adheres to
88

99
## [Unreleased]
1010

11-
- ⚡️(frontend) improve accessibility:
12-
- #1341
13-
1411
### Added
1512

1613
- ✨(api) add API route to fetch document content #1206
@@ -19,6 +16,10 @@ and this project adheres to
1916

2017
- 🔒️(backend) configure throttle on every viewsets #1343
2118
- ⬆️ Bump eslint to V9 #1071
19+
- ♿(frontend) improve accessibility:
20+
- #1354
21+
- #1341
22+
2223

2324
## [3.6.0] - 2025-09-04
2425

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,10 @@ test.describe('Document search', () => {
175175

176176
// Expect to find the first doc
177177
await expect(
178-
page.getByRole('presentation').getByLabel(firstDocTitle),
178+
page.getByRole('presentation').getByText(firstDocTitle),
179179
).toBeVisible();
180180
await expect(
181-
page.getByRole('presentation').getByLabel(secondDocTitle),
181+
page.getByRole('presentation').getByText(secondDocTitle),
182182
).toBeVisible();
183183

184184
await page.getByRole('button', { name: 'close' }).click();
@@ -196,13 +196,11 @@ test.describe('Document search', () => {
196196

197197
// Now there is a sub page - expect to have the focus on the current doc
198198
await expect(
199-
page.getByRole('presentation').getByLabel(secondDocTitle),
199+
page.getByRole('presentation').getByText(secondDocTitle),
200200
).toBeVisible();
201+
await expect(page.getByText(secondChildDocTitle)).toBeVisible();
201202
await expect(
202-
page.getByRole('presentation').getByLabel(secondChildDocTitle),
203-
).toBeVisible();
204-
await expect(
205-
page.getByRole('presentation').getByLabel(firstDocTitle),
203+
page.getByRole('presentation').getByText(firstDocTitle),
206204
).toBeHidden();
207205
});
208206
});

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,46 @@ test.describe('Doc Tree', () => {
252252
page.getByRole('menuitem', { name: 'Move to my docs' }),
253253
).toHaveAttribute('aria-disabled', 'true');
254254
});
255+
256+
test('keyboard navigation with Enter key opens documents', async ({
257+
page,
258+
browserName,
259+
}) => {
260+
// Create a parent document
261+
const [docParent] = await createDoc(
262+
page,
263+
'doc-tree-keyboard-nav',
264+
browserName,
265+
1,
266+
);
267+
await verifyDocName(page, docParent);
268+
269+
// Create a sub-document
270+
const { name: docChild } = await createRootSubPage(
271+
page,
272+
browserName,
273+
'doc-tree-keyboard-child',
274+
);
275+
276+
const docTree = page.getByTestId('doc-tree');
277+
await expect(docTree).toBeVisible();
278+
279+
// Test keyboard navigation on root document
280+
const rootItem = page.getByTestId('doc-tree-root-item');
281+
await expect(rootItem).toBeVisible();
282+
283+
// Focus on the root item and press Enter
284+
await rootItem.focus();
285+
await expect(rootItem).toBeFocused();
286+
await page.keyboard.press('Enter');
287+
288+
// Verify we navigated to the root document
289+
await verifyDocName(page, docParent);
290+
await expect(page).toHaveURL(/\/docs\/[^/]+\/?$/);
291+
292+
// Now test keyboard navigation on sub-document
293+
await expect(docTree.getByText(docChild)).toBeVisible();
294+
});
255295
});
256296

257297
test.describe('Doc Tree: Inheritance', () => {

src/frontend/apps/impress/src/features/docs/doc-management/components/SimpleDocItem.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { DateTime } from 'luxon';
2+
import { useRouter } from 'next/navigation';
23
import { useTranslation } from 'react-i18next';
34
import { css } from 'styled-components';
45

@@ -26,17 +27,28 @@ type SimpleDocItemProps = {
2627
doc: Doc;
2728
isPinned?: boolean;
2829
showAccesses?: boolean;
30+
onActivate?: () => void;
2931
};
3032

3133
export const SimpleDocItem = ({
3234
doc,
3335
isPinned = false,
3436
showAccesses = false,
37+
onActivate,
3538
}: SimpleDocItemProps) => {
3639
const { t } = useTranslation();
3740
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
3841
const { isDesktop } = useResponsiveStore();
3942
const { untitledDocument } = useTrans();
43+
const router = useRouter();
44+
45+
const handleActivate = () => {
46+
if (onActivate) {
47+
onActivate();
48+
} else {
49+
router.push(`/docs/${doc.id}`);
50+
}
51+
};
4052

4153
const { emoji, titleWithoutEmoji: displayTitle } = getEmojiAndTitle(
4254
doc.title || untitledDocument,
@@ -49,6 +61,9 @@ export const SimpleDocItem = ({
4961
$overflow="auto"
5062
$width="100%"
5163
className="--docs--simple-doc-item"
64+
role="presentation"
65+
onClick={handleActivate}
66+
aria-label={`${t('Open document')} ${doc.title || untitledDocument}`}
5267
>
5368
<Box
5469
$direction="row"
@@ -59,6 +74,7 @@ export const SimpleDocItem = ({
5974
`}
6075
$padding={`${spacingsTokens['3xs']} 0`}
6176
data-testid={isPinned ? `doc-pinned-${doc.id}` : undefined}
77+
aria-hidden="true"
6278
>
6379
{isPinned ? (
6480
<PinnedDocumentIcon
@@ -97,6 +113,7 @@ export const SimpleDocItem = ({
97113
$align="center"
98114
$gap={spacingsTokens['3xs']}
99115
$margin={{ top: '-2px' }}
116+
aria-hidden="true"
100117
>
101118
<Text $variation="600" $size="xs">
102119
{DateTime.fromISO(doc.updated_at).toRelative()}

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

Lines changed: 69 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import {
55
} from '@gouvfr-lasuite/ui-kit';
66
import { useRouter } from 'next/navigation';
77
import { useState } from 'react';
8+
import { useTranslation } from 'react-i18next';
89
import { css } from 'styled-components';
910

10-
import { Box, Icon, Text } from '@/components';
11+
import { Box, BoxButton, Icon, Text } from '@/components';
1112
import { useCunninghamTheme } from '@/cunningham';
1213
import {
1314
Doc,
@@ -20,6 +21,7 @@ import { useResponsiveStore } from '@/stores';
2021

2122
import SubPageIcon from './../assets/sub-page-logo.svg';
2223
import { DocTreeItemActions } from './DocTreeItemActions';
24+
import { useKeyboardActivation } from './hooks/useKeyboardActivation';
2325

2426
const ItemTextCss = css`
2527
overflow: hidden;
@@ -38,14 +40,23 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
3840
const { node } = props;
3941
const { spacingsTokens } = useCunninghamTheme();
4042
const { isDesktop } = useResponsiveStore();
41-
const [actionsOpen, setActionsOpen] = useState(false);
43+
const { t } = useTranslation();
44+
45+
const [menuOpen, setMenuOpen] = useState(false);
46+
const isSelectedNow = treeContext?.treeData.selectedNode?.id === doc.id;
47+
const isActive = node.isFocused || menuOpen || isSelectedNow;
4248

4349
const router = useRouter();
4450
const { togglePanel } = useLeftPanelStore();
4551

4652
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(doc.title || '');
4753
const displayTitle = titleWithoutEmoji || untitledDocument;
4854

55+
const handleActivate = () => {
56+
treeContext?.treeData.setSelectedNode(doc);
57+
router.push(`/docs/${doc.id}`);
58+
};
59+
4960
const afterCreate = (createdDoc: Doc) => {
5061
const actualChildren = node.data.children ?? [];
5162

@@ -76,62 +87,66 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
7687
}
7788
};
7889

90+
useKeyboardActivation(
91+
['Enter', ' '],
92+
isActive && !menuOpen,
93+
handleActivate,
94+
true,
95+
);
96+
97+
const docTitle = doc.title || untitledDocument;
98+
const hasChildren = (doc.children?.length || 0) > 0;
99+
const isExpanded = node.isOpen;
100+
const isSelected = isSelectedNow;
101+
const ariaLabel = docTitle;
102+
79103
return (
80104
<Box
81105
className="--docs-sub-page-item"
82106
draggable={doc.abilities.move && isDesktop}
83107
$position="relative"
108+
role="treeitem"
109+
aria-label={ariaLabel}
110+
aria-selected={isSelected}
111+
aria-expanded={hasChildren ? isExpanded : undefined}
84112
$css={css`
85-
background-color: ${actionsOpen
86-
? 'var(--c--theme--colors--greyscale-100)'
87-
: 'var(--c--theme--colors--greyscale-000)'};
88-
89-
.light-doc-item-actions {
90-
display: ${actionsOpen || !isDesktop ? 'flex' : 'none'};
91-
position: absolute;
92-
right: 0;
93-
background: ${isDesktop
94-
? 'var(--c--theme--colors--greyscale-100)'
95-
: 'var(--c--theme--colors--greyscale-000)'};
96-
}
97-
98-
.c__tree-view--node.isSelected {
99-
.light-doc-item-actions {
100-
background: var(--c--theme--colors--greyscale-100);
101-
}
113+
.c__tree-view--node {
114+
padding: ${spacingsTokens['3xs']};
115+
border-radius: 4px;
102116
}
103117
104-
&:hover {
118+
.c__tree-view--node:hover {
105119
background-color: var(--c--theme--colors--greyscale-100);
106-
border-radius: 4px;
120+
}
107121
108-
.light-doc-item-actions {
109-
display: flex;
110-
background: var(--c--theme--colors--greyscale-100);
111-
}
122+
.light-doc-item-actions {
123+
display: flex;
124+
opacity: ${isActive || !isDesktop ? 1 : 0};
125+
position: absolute;
126+
right: 0;
127+
top: 0;
128+
height: 100%;
129+
z-index: 10;
112130
}
113131
114132
.row.preview & {
115133
background-color: inherit;
116134
}
117135
`}
118136
>
119-
<TreeViewItem
120-
{...props}
121-
onClick={() => {
122-
treeContext?.treeData.setSelectedNode(props.node.data.value as Doc);
123-
router.push(`/docs/${props.node.data.value.id}`);
124-
}}
125-
>
126-
<Box
127-
data-testid={`doc-sub-page-item-${props.node.data.value.id}`}
137+
<TreeViewItem {...props} onClick={handleActivate}>
138+
<BoxButton
139+
onClick={(e) => {
140+
e.stopPropagation();
141+
handleActivate();
142+
}}
128143
$width="100%"
129144
$direction="row"
130145
$gap={spacingsTokens['xs']}
131-
role="button"
132-
tabIndex={0}
133146
$align="center"
134147
$minHeight="24px"
148+
data-testid={`doc-sub-page-item-${doc.id}`}
149+
aria-label={`${t('Open document')} ${docTitle}`}
135150
>
136151
<Box $width="16px" $height="16px">
137152
<DocIcon emoji={emoji} defaultIcon={<SubPageIcon />} $size="sm" />
@@ -157,25 +172,28 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
157172
iconName="group"
158173
$size="16px"
159174
$variation="400"
175+
aria-hidden="true"
160176
/>
161177
)}
162178
</Box>
163-
164-
<Box
165-
$direction="row"
166-
$align="center"
167-
className="light-doc-item-actions"
168-
>
169-
<DocTreeItemActions
170-
doc={doc}
171-
isOpen={actionsOpen}
172-
onOpenChange={setActionsOpen}
173-
parentId={node.data.parentKey}
174-
onCreateSuccess={afterCreate}
175-
/>
176-
</Box>
177-
</Box>
179+
</BoxButton>
178180
</TreeViewItem>
181+
182+
<Box
183+
$direction="row"
184+
$align="center"
185+
className="light-doc-item-actions"
186+
role="toolbar"
187+
aria-label={`${t('Actions for')} ${docTitle}`}
188+
>
189+
<DocTreeItemActions
190+
doc={doc}
191+
isOpen={menuOpen}
192+
onOpenChange={setMenuOpen}
193+
parentId={node.data.parentKey}
194+
onCreateSuccess={afterCreate}
195+
/>
196+
</Box>
179197
</Box>
180198
);
181199
};

0 commit comments

Comments
 (0)