Skip to content

Commit 53ceb7a

Browse files
committed
⚡️(frontend) improve accessibility of selected document's sub-menu
adds focus style to make the sub-menu accessible unify dropdownmenu Signed-off-by: Cyril <[email protected]>
1 parent 0cf8b9d commit 53ceb7a

File tree

8 files changed

+92
-31
lines changed

8 files changed

+92
-31
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ and this project adheres to
2323
- 🐛(makefile) Windows compatibility fix for Docker volume mounting #1264
2424
- 🐛(minio) fix user permission error with Minio and Windows #1264
2525

26+
- #1261
27+
2628

2729
## [3.5.0] - 2025-07-31
2830

src/frontend/apps/e2e/__tests__/app-impress/utils-sub-pages.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,6 @@ export const clickOnAddRootSubPage = async (page: Page) => {
6363
const rootItem = page.getByTestId('doc-tree-root-item');
6464
await expect(rootItem).toBeVisible();
6565
await rootItem.hover();
66-
await rootItem.getByRole('button', { name: 'add_box' }).click();
66+
67+
await rootItem.getByTestId('add-child-doc').click();
6768
};

src/frontend/apps/impress/src/components/dropdown-menu/DropdownMenu.tsx

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { HorizontalSeparator } from '@gouvfr-lasuite/ui-kit';
22
import {
33
Fragment,
44
PropsWithChildren,
5+
ReactNode,
56
useCallback,
67
useEffect,
78
useRef,
@@ -15,7 +16,7 @@ import { useCunninghamTheme } from '@/cunningham';
1516
import { useDropdownKeyboardNav } from './hook/useDropdownKeyboardNav';
1617

1718
export type DropdownMenuOption = {
18-
icon?: string;
19+
icon?: string | ReactNode;
1920
label: string;
2021
testId?: string;
2122
value?: string;
@@ -79,14 +80,28 @@ export const DropdownMenu = ({
7980

8081
// Focus selected menu item when menu opens
8182
useEffect(() => {
82-
if (isOpen && menuItemRefs.current.length > 0) {
83-
const selectedIndex = options.findIndex((option) => option.isSelected);
84-
if (selectedIndex !== -1) {
85-
setFocusedIndex(selectedIndex);
86-
setTimeout(() => {
87-
menuItemRefs.current[selectedIndex]?.focus();
88-
}, 0);
89-
}
83+
if (!isOpen || menuItemRefs.current.length === 0) {
84+
return;
85+
}
86+
87+
const selectedIndex = options.findIndex((option) => option.isSelected);
88+
if (selectedIndex !== -1) {
89+
setFocusedIndex(selectedIndex);
90+
setTimeout(() => {
91+
menuItemRefs.current[selectedIndex]?.focus();
92+
}, 0);
93+
return;
94+
}
95+
96+
// Fallback: focus first enabled/visible option
97+
const firstEnabledIndex = options.findIndex(
98+
(opt) => opt.show !== false && !opt.disabled,
99+
);
100+
if (firstEnabledIndex !== -1) {
101+
setFocusedIndex(firstEnabledIndex);
102+
setTimeout(() => {
103+
menuItemRefs.current[firstEnabledIndex]?.focus();
104+
}, 0);
90105
}
91106
}, [isOpen, options]);
92107

@@ -153,7 +168,6 @@ export const DropdownMenu = ({
153168
return;
154169
}
155170
const isDisabled = option.disabled !== undefined && option.disabled;
156-
const isFocused = index === focusedIndex;
157171

158172
return (
159173
<Fragment key={option.label}>
@@ -204,32 +218,26 @@ export const DropdownMenu = ({
204218
}
205219
206220
&:focus-visible {
207-
outline: 2px solid var(--c--theme--colors--primary-500);
208-
outline-offset: -2px;
209221
background-color: var(--c--theme--colors--greyscale-050);
210222
}
211-
212-
${isFocused &&
213-
css`
214-
outline: 2px solid var(--c--theme--colors--primary-500);
215-
outline-offset: -2px;
216-
background-color: var(--c--theme--colors--greyscale-050);
217-
`}
218223
`}
219224
>
220225
<Box
221226
$direction="row"
222227
$align="center"
223228
$gap={spacingsTokens['base']}
224229
>
225-
{option.icon && (
226-
<Icon
227-
$size="20px"
228-
$theme="greyscale"
229-
$variation={isDisabled ? '400' : '1000'}
230-
iconName={option.icon}
231-
/>
232-
)}
230+
{option.icon &&
231+
(typeof option.icon === 'string' ? (
232+
<Icon
233+
$size="20px"
234+
$theme="greyscale"
235+
$variation={isDisabled ? '400' : '1000'}
236+
iconName={option.icon}
237+
/>
238+
) : (
239+
option.icon
240+
))}
233241
<Text $variation={isDisabled ? '400' : '1000'}>
234242
{option.label}
235243
</Text>

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,14 @@ export const SimpleDocItem = ({
5959
aria-hidden="true"
6060
aria-label={t('Pin document icon')}
6161
color={colorsTokens['primary-500']}
62+
aria-hidden="true"
6263
/>
6364
) : (
6465
<SimpleFileIcon
6566
aria-hidden="true"
6667
aria-label={t('Simple document icon')}
6768
color={colorsTokens['primary-500']}
69+
aria-hidden="true"
6870
/>
6971
)}
7072
</Box>

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,19 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
8787
: 'var(--c--theme--colors--greyscale-000)'};
8888
}
8989
90+
&:focus-within .light-doc-item-actions {
91+
display: flex;
92+
background: var(--c--theme--colors--greyscale-100);
93+
}
94+
9095
.c__tree-view--node.isSelected {
9196
.light-doc-item-actions {
9297
background: var(--c--theme--colors--greyscale-100);
9398
}
9499
}
95100
96-
&:hover {
101+
&:hover,
102+
&:focus-within {
97103
background-color: var(--c--theme--colors--greyscale-100);
98104
border-radius: 4px;
99105

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
189189
opacity: 1;
190190
}
191191
}
192-
&:hover {
192+
&:hover,
193+
&:focus-within {
193194
.doc-tree-root-item-actions {
194195
opacity: 1;
195196
}

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,25 @@ export const DocTreeItemActions = ({
150150
$align="center"
151151
className="--docs--doc-tree-item-actions"
152152
$gap="4px"
153+
$css={css`
154+
&:focus-within {
155+
opacity: 1;
156+
visibility: visible;
157+
}
158+
button:focus-visible,
159+
[role='button']:focus-visible {
160+
outline: 2px solid var(--c--theme--colors--primary-500);
161+
outline-offset: 2px;
162+
background-color: var(--c--theme--colors--greyscale-050);
163+
border-radius: 4px;
164+
}
165+
.icon-button:focus-visible {
166+
outline: 2px solid var(--c--theme--colors--primary-500);
167+
outline-offset: 2px;
168+
background-color: var(--c--theme--colors--greyscale-050);
169+
border-radius: 4px;
170+
}
171+
`}
153172
>
154173
<DropdownMenu
155174
options={options}
@@ -166,10 +185,22 @@ export const DocTreeItemActions = ({
166185
variant="filled"
167186
$theme="primary"
168187
$variation="600"
188+
className="icon-button"
189+
tabIndex={0}
190+
role="button"
191+
aria-label={t('More options')}
192+
onKeyDown={(e) => {
193+
if (e.key === 'Enter' || e.key === ' ') {
194+
e.preventDefault();
195+
e.stopPropagation();
196+
onOpenChange?.(!isOpen);
197+
}
198+
}}
169199
/>
170200
</DropdownMenu>
171201
{doc.abilities.children_create && (
172202
<BoxButton
203+
data-testid="add-child-doc"
173204
onClick={(e) => {
174205
e.stopPropagation();
175206
e.preventDefault();
@@ -179,6 +210,7 @@ export const DocTreeItemActions = ({
179210
});
180211
}}
181212
color="primary"
213+
aria-label={t('Add child document')}
182214
>
183215
<Icon
184216
variant="filled"

src/frontend/apps/impress/src/i18n/translations.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,8 @@
270270
"Move": "Verschieben",
271271
"Move document": "Dokument verschieben",
272272
"Move to my docs": "In \"Meine Dokumente\" verschieben",
273+
"More options": "Weitere Optionen",
274+
"Add child document": "Unterdokument hinzufügen",
273275
"My docs": "Meine Dokumente",
274276
"Name": "Name",
275277
"New doc": "Neues Dokument",
@@ -381,7 +383,8 @@
381383
"Shared with {{count}} users_many": "Shared with {{count}} users",
382384
"Shared with {{count}} users_one": "Shared with {{count}} user",
383385
"Shared with {{count}} users_other": "Shared with {{count}} users",
384-
"Updated": "Updated"
386+
"Updated": "Updated",
387+
"Add child document": "Add child document"
385388
}
386389
},
387390
"es": {
@@ -566,6 +569,8 @@
566569
"You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.": "Eres el único propietario de este grupo, haz que otro miembro sea el propietario del grupo para poder cambiar tu propio rol o ser eliminado del documento.",
567570
"Your current document will revert to this version.": "Tu documento actual se revertirá a esta versión.",
568571
"Your {{format}} was downloaded succesfully": "Su {{format}} se ha descargado correctamente",
572+
"More options": "Más opciones",
573+
"Add child document": "Añadir documento hijo",
569574
"home-content-open-source-part1": "Docs está construido sobre <2>Django Rest Framework</2> y <6>Next.js</6>. También utilizamos <9>Yjs</9> y <13>BlockNote.js</13>, dos proyectos que estamos orgullosos de patrocinar.",
570575
"home-content-open-source-part2": "Puede autoalojar fácilmente Docs (consulte nuestra <2>documentación</2> de instalación).<br/>Docs utiliza una <7>licencia</7> (MIT) adecuada para la innovación y las empresas.<br/>Se aceptan contribuciones (consulte nuestra hoja de ruta <13>aquí</13>).",
571576
"home-content-open-source-part3": "Docs es el resultado de un esfuerzo conjunto llevado a cabo por los gobiernos francés 🇫🇷🥖 <1>(DINUM)</1> y alemán 🇩🇪🥨 <5>(ZenDiS)</5>."
@@ -702,6 +707,8 @@
702707
"Move": "Déplacer",
703708
"Move document": "Déplacer le document",
704709
"Move to my docs": "Déplacer vers mes docs",
710+
"More options": "Plus d'options",
711+
"Add child document": "Ajouter un document enfant",
705712
"My docs": "Mes documents",
706713
"Name": "Nom",
707714
"New doc": "Nouveau doc",
@@ -1133,6 +1140,8 @@
11331140
"You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.": "U bent de enige eigenaar van deze groep, maak een ander lid de groepseigenaar voordat u uw eigen rol kunt wijzigen of kan worden verwijderd van het document.",
11341141
"Your current document will revert to this version.": "Uw huidige document wordt teruggezet naar deze versie.",
11351142
"Your {{format}} was downloaded succesfully": "Jouw {{format}} is succesvol gedownload",
1143+
"More options": "Meer opties",
1144+
"Add child document": "Onderliggend document toevoegen",
11361145
"home-content-open-source-part1": "Docs is gebouwd op <2>Django Rest Framework</2> en <6>Next.js</6>. We gebruiken ook <9>Yjs</9> en <13>BlockNote.js</13>, twee projecten die we met trots sponsoren.",
11371146
"home-content-open-source-part2": "U kunt Docs eenvoudig zelf hosten (zie onze <2>installatiedocumentatie</2>).<br/>Docs gebruikt een <7>licentie</7> (MIT) die is afgestemd op innovatie en ondernemingen.<br/>Bijdragen zijn welkom (zie onze routekaart <13>hier</13>).",
11381147
"home-content-open-source-part3": "Docs is het resultaat van een gezamenlijke inspanning geleid door de Franse 🇫🇷🥖 <1>(DINUM)</1> en Duitse 🇩🇪🥨 <5>(ZenDiS)</5> overheden."

0 commit comments

Comments
 (0)