Skip to content

Commit 6d0eba4

Browse files
committed
✨(frontend) make components accessible to screen readers
adds proper aria props and translation keys for accessibility support Signed-off-by: Cyril <[email protected]>
1 parent dcc07fa commit 6d0eba4

File tree

8 files changed

+163
-48
lines changed

8 files changed

+163
-48
lines changed

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

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
updateDocTitle,
1010
verifyDocName,
1111
} from './utils-common';
12-
import { addNewMember } from './utils-share';
1312
import { clickOnAddRootSubPage, createRootSubPage } from './utils-sub-pages';
1413

1514
test.describe('Doc Tree', () => {
@@ -185,14 +184,13 @@ test.describe('Doc Tree', () => {
185184
const docTree = page.getByTestId('doc-tree');
186185
await expect(docTree.getByText(docChild)).toBeVisible();
187186
await docTree.click();
188-
const child = docTree
189-
.getByRole('treeitem')
190-
.locator('.--docs-sub-page-item')
191-
.filter({
192-
hasText: docChild,
193-
});
187+
const child = docTree.locator('.--docs-sub-page-item').filter({
188+
hasText: docChild,
189+
});
194190
await child.hover();
191+
// Wait a bit for the hover effect to take place
195192
const menu = child.getByText(`more_horiz`);
193+
await expect(menu).toBeVisible();
196194
await menu.click();
197195
await page.getByText('Move to my docs').click();
198196

@@ -215,43 +213,38 @@ test.describe('Doc Tree', () => {
215213

216214
await verifyDocName(page, docParent);
217215

218-
await page.getByRole('button', { name: 'Share' }).click();
219-
220-
await addNewMember(page, 0, 'Owner', 'impress');
221-
222-
const list = page.getByTestId('doc-share-quick-search');
223-
const currentUser = list.getByTestId(
224-
`doc-share-member-row-user@${browserName}.test`,
225-
);
226-
const currentUserRole = currentUser.getByLabel('doc-role-dropdown');
227-
await currentUserRole.click();
228-
await page.getByLabel('Administrator').click();
229-
await list.click();
230-
231-
await page.getByRole('button', { name: 'Ok' }).click();
232-
216+
// Create a child document first
233217
const { name: docChild } = await createRootSubPage(
234218
page,
235219
browserName,
236220
'doc-tree-detach-child',
237221
);
238222

223+
// Now try to detach the child document - this should work for the owner
239224
const docTree = page.getByTestId('doc-tree');
240225
await expect(docTree.getByText(docChild)).toBeVisible();
241226
await docTree.click();
242-
const child = docTree
243-
.getByRole('treeitem')
244-
.locator('.--docs-sub-page-item')
245-
.filter({
246-
hasText: docChild,
247-
});
227+
const child = docTree.locator('.--docs-sub-page-item').filter({
228+
hasText: docChild,
229+
});
248230
await child.hover();
231+
// Wait a bit for the hover effect to take place
249232
const menu = child.getByText(`more_horiz`);
233+
await expect(menu).toBeVisible();
250234
await menu.click();
251235

236+
// The owner should be able to detach the document
237+
await page.getByRole('menuitem', { name: 'Move to my docs' }).click();
238+
239+
// Verify the document was detached - it should no longer be in the current tree
252240
await expect(
253-
page.getByRole('menuitem', { name: 'Move to my docs' }),
254-
).toHaveAttribute('aria-disabled', 'true');
241+
page.getByRole('textbox', { name: 'doc title input' }),
242+
).not.toHaveText(docChild);
243+
244+
// Verify the document is now on the home page
245+
const header = page.locator('header').first();
246+
await header.locator('h1').getByText('Docs').click();
247+
await expect(page.getByText(docChild)).toBeVisible();
255248
});
256249
});
257250

src/frontend/apps/impress/src/components/dropdown-menu/hook/useActionableMode.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,6 @@ type FocusableNode<T> = NodeRendererProps<TreeDataItem<T>>['node'] & {
1010
/**
1111
* Hook to manage keyboard navigation for actionable items in a tree view.
1212
*
13-
* Provides two modes:
14-
* 1. Activation: F2/Enter moves focus to first actionable element
15-
* 2. Navigation: Arrow keys navigate between actions, Escape returns to tree node
16-
*
1713
* Disables navigation when dropdown menu is open to prevent conflicts.
1814
*/
1915
export const useActionableMode = <T>(
@@ -23,11 +19,21 @@ export const useActionableMode = <T>(
2319
const actionsRef = useRef<HTMLDivElement>(null);
2420

2521
useEffect(() => {
26-
if (!node?.isFocused) {
22+
const modalOpen = document.querySelector(
23+
'[role="dialog"], .c__modal, [data-modal], .c__modal__overlay, .ReactModal_Content',
24+
);
25+
if (!node?.isFocused || modalOpen) {
2726
return;
2827
}
2928

3029
const toActions = (e: KeyboardEvent) => {
30+
const modalOpen = document.querySelector(
31+
'[role="dialog"], .c__modal, [data-modal], .c__modal__overlay, .ReactModal_Content',
32+
);
33+
if (modalOpen) {
34+
return;
35+
}
36+
3137
if (e.key === 'F2' || e.key === 'Enter') {
3238
const isAlreadyInActions = actionsRef.current?.contains(
3339
document.activeElement,
@@ -61,6 +67,13 @@ export const useActionableMode = <T>(
6167
return;
6268
}
6369

70+
const modal = document.querySelector(
71+
'[role="dialog"], .c__modal, [data-modal], .c__modal__overlay, .ReactModal_Content',
72+
);
73+
if (modal) {
74+
return;
75+
}
76+
6477
if (e.key === 'Escape') {
6578
e.stopPropagation();
6679
node?.focus?.();

src/frontend/apps/impress/src/components/dropdown-menu/hook/useDropdownFocusManagement.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,14 @@ export const useDropdownFocusManagement = ({
5656
}
5757

5858
const timer = setTimeout(() => {
59-
// Try to find sub-document by closest ancestor
59+
const modal = document.querySelector(
60+
'[role="dialog"], .c__modal, [data-modal], .c__modal__overlay, .ReactModal_Content',
61+
);
62+
if (modal) {
63+
return;
64+
}
65+
66+
// Only handle focus return if no modal is open
6067
let subPageItem = actionsRef?.current?.closest('.--docs-sub-page-item');
6168

6269
// If not found, try to find by data-testid

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const SimpleDocItem = ({
4343
$overflow="auto"
4444
$width="100%"
4545
className="--docs--simple-doc-item"
46+
role="presentation"
4647
>
4748
<Box
4849
$direction="row"
@@ -53,31 +54,29 @@ export const SimpleDocItem = ({
5354
`}
5455
$padding={`${spacingsTokens['3xs']} 0`}
5556
data-testid={isPinned ? `doc-pinned-${doc.id}` : undefined}
57+
aria-hidden="true"
5658
>
5759
{isPinned ? (
5860
<PinnedDocumentIcon
5961
aria-hidden="true"
6062
aria-label={t('Pin document icon')}
6163
color={colorsTokens['primary-500']}
62-
aria-hidden="true"
6364
/>
6465
) : (
6566
<SimpleFileIcon
6667
aria-hidden="true"
6768
aria-label={t('Simple document icon')}
6869
color={colorsTokens['primary-500']}
69-
aria-hidden="true"
7070
/>
7171
)}
7272
</Box>
7373
<Box $justify="center" $overflow="auto">
7474
<Text
75-
aria-describedby="doc-title"
76-
aria-label={doc.title}
7775
$size="sm"
7876
$variation="1000"
7977
$weight="500"
8078
$css={ItemTextCss}
79+
aria-describedby="doc-title"
8180
>
8281
{doc.title || untitledDocument}
8382
</Text>
@@ -87,6 +86,7 @@ export const SimpleDocItem = ({
8786
$align="center"
8887
$gap={spacingsTokens['3xs']}
8988
$margin={{ top: '-2px' }}
89+
aria-hidden="true"
9090
>
9191
<Text $variation="600" $size="xs">
9292
{DateTime.fromISO(doc.updated_at).toRelative()}

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

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ 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

1011
import { Box, BoxButton, Icon, Text } from '@/components';
@@ -35,6 +36,7 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
3536
const { node } = props;
3637
const { spacingsTokens } = useCunninghamTheme();
3738
const { isDesktop } = useResponsiveStore();
39+
const { t } = useTranslation();
3840

3941
const [menuOpen, setMenuOpen] = useState(false);
4042

@@ -80,11 +82,23 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
8082

8183
useTreeItemKeyboardActivate(isActive, handleActivate);
8284

85+
// prepare the text for the screen reader
86+
const docTitle = doc.title || untitledDocument;
87+
const hasChildren = (doc.children?.length || 0) > 0;
88+
const isExpanded = node.isOpen;
89+
const isSelected = treeContext?.treeData.selectedNode?.id === doc.id;
90+
91+
const ariaLabel = `${docTitle}${hasChildren ? `, ${isExpanded ? t('expanded') : t('collapsed')}` : ''}${isSelected ? `, ${t('selected')}` : ''}`;
92+
8393
return (
8494
<Box
8595
className="--docs-sub-page-item"
8696
draggable={doc.abilities.move && isDesktop}
8797
$position="relative"
98+
role="treeitem"
99+
aria-label={ariaLabel}
100+
aria-selected={isSelected}
101+
aria-expanded={hasChildren ? isExpanded : undefined}
88102
$css={css`
89103
background-color: ${isActive
90104
? 'var(--c--theme--colors--greyscale-100)'
@@ -106,6 +120,8 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
106120
107121
&:focus-within .light-doc-item-actions {
108122
display: flex;
123+
opacity: 1;
124+
visibility: visible;
109125
background: var(--c--theme--colors--greyscale-100);
110126
}
111127
@@ -122,14 +138,15 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
122138
123139
.light-doc-item-actions {
124140
display: flex;
141+
opacity: 1;
142+
visibility: visible;
125143
background: var(--c--theme--colors--greyscale-100);
126144
}
127145
}
128146
`}
129147
>
130148
<TreeViewItem {...props} onClick={handleActivate}>
131149
<BoxButton
132-
as="button"
133150
onClick={(e) => {
134151
e.stopPropagation();
135152
handleActivate();
@@ -140,8 +157,10 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
140157
$align="center"
141158
$minHeight="24px"
142159
data-testid={`doc-sub-page-item-${doc.id}`}
160+
aria-label={`${t('Open document')} ${docTitle}`}
161+
role="button"
143162
>
144-
<Box $width="16px" $height="16px">
163+
<Box $width="16px" $height="16px" aria-hidden="true">
145164
<SubPageIcon />
146165
</Box>
147166

@@ -156,8 +175,13 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
156175
align-items: center;
157176
`}
158177
>
159-
<Text $css={ItemTextCss} $size="sm" $variation="1000">
160-
{doc.title || untitledDocument}
178+
<Text
179+
$css={ItemTextCss}
180+
$size="sm"
181+
$variation="1000"
182+
aria-hidden="true"
183+
>
184+
{docTitle}
161185
</Text>
162186
{doc.nb_accesses_direct >= 1 && (
163187
<Icon
@@ -178,6 +202,8 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
178202
$direction="row"
179203
$align="center"
180204
className="light-doc-item-actions"
205+
role="group"
206+
aria-label={`${t('Actions for')} ${docTitle}`}
181207
>
182208
<DocTreeItemActions
183209
doc={doc}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from '@gouvfr-lasuite/ui-kit';
88
import { useRouter } from 'next/navigation';
99
import { useCallback, useEffect, useState } from 'react';
10+
import { useTranslation } from 'react-i18next';
1011
import { css } from 'styled-components';
1112

1213
import { Box, StyledLink } from '@/components';
@@ -30,6 +31,7 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
3031
const treeContext = useTreeContext<Doc | null>();
3132
const router = useRouter();
3233
const { isDesktop } = useResponsive();
34+
const { t } = useTranslation();
3335

3436
const [initialOpenState, setInitialOpenState] = useState<OpenMap | undefined>(
3537
undefined,
@@ -150,6 +152,8 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
150152
<Box
151153
data-testid="doc-tree"
152154
$height="100%"
155+
role="tree"
156+
aria-label={t('Document tree')}
153157
$css={css`
154158
.c__tree-view--container {
155159
z-index: 1;
@@ -169,6 +173,9 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
169173
>
170174
<Box
171175
data-testid="doc-tree-root-item"
176+
role="treeitem"
177+
aria-label={`${t('Root document')}: ${treeContext.root?.title || t('Untitled document')}`}
178+
aria-selected={rootIsSelected}
172179
$css={css`
173180
padding: ${spacingsTokens['2xs']};
174181
border-radius: 4px;
@@ -210,6 +217,7 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
210217
);
211218
router.push(`/docs/${treeContext?.root?.id}`);
212219
}}
220+
aria-label={`${t('Open root document')}: ${treeContext.root?.title || t('Untitled document')}`}
213221
>
214222
<Box $direction="row" $align="center" $width="100%">
215223
<SimpleDocItem doc={treeContext.root} showAccesses={true} />

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,11 @@ export const DocTreeItemActions = ({
211211
className="icon-button"
212212
tabIndex={0}
213213
role="button"
214-
aria-label={t('More options')}
214+
aria-label={
215+
t('More options for') + ` ${doc.title || t('Untitled document')}`
216+
}
217+
aria-haspopup="true"
218+
aria-expanded={isOpen}
215219
onKeyDown={handleMoreOptionsKeyDown}
216220
/>
217221
</DropdownMenu>
@@ -223,14 +227,18 @@ export const DocTreeItemActions = ({
223227
onClick={handleAddChildClick}
224228
onKeyDown={handleAddChildKeyDown}
225229
color="primary"
226-
aria-label={t('Add child document')}
230+
aria-label={
231+
t('Add child document to') +
232+
` ${doc.title || t('Untitled document')}`
233+
}
227234
$hasTransition={false}
228235
>
229236
<Icon
230237
variant="filled"
231238
$variation="800"
232239
$theme="primary"
233240
iconName="add_box"
241+
aria-hidden="true"
234242
/>
235243
</BoxButton>
236244
)}

0 commit comments

Comments
 (0)