Skip to content

Commit 70956fb

Browse files
committed
✨(frontend) add focus trap and enter key support to remove doc modal
improves accessibility by enabling keyboard-triggered modal with proper focus trap Signed-off-by: Cyril <[email protected]>
1 parent 2f010cf commit 70956fb

File tree

4 files changed

+69
-8
lines changed

4 files changed

+69
-8
lines changed

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { HorizontalSeparator } from '@gouvfr-lasuite/ui-kit';
22
import {
33
Fragment,
4+
KeyboardEvent,
45
PropsWithChildren,
56
ReactNode,
67
useCallback,
@@ -93,6 +94,18 @@ export const DropdownMenu = ({
9394
}
9495
}, [isOpen, options]);
9596

97+
const handleKeyDown = useCallback(
98+
(event: KeyboardEvent<HTMLElement>, option: DropdownMenuOption) => {
99+
if (event.key === 'Enter' || event.key === ' ') {
100+
event.preventDefault();
101+
event.stopPropagation();
102+
onOpenChange?.(false);
103+
void option.callback?.();
104+
}
105+
},
106+
[onOpenChange],
107+
);
108+
96109
if (disabled) {
97110
return children;
98111
}
@@ -173,6 +186,7 @@ export const DropdownMenu = ({
173186
onOpenChange?.(false);
174187
void option.callback?.();
175188
}}
189+
onKeyDown={(event) => handleKeyDown(event, option)}
176190
key={option.label}
177191
$align="center"
178192
$justify="space-between"

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

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,21 @@ import {
66
useToastProvider,
77
} from '@openfun/cunningham-react';
88
import { useRouter } from 'next/router';
9+
import { useEffect } from 'react';
910
import { Trans, useTranslation } from 'react-i18next';
1011

1112
import { Box, ButtonCloseModal, Text, TextErrors } from '@/components';
1213
import { useConfig } from '@/core';
1314
import { KEY_LIST_DOC_TRASHBIN } from '@/docs/docs-grid';
15+
import { useKeyboardAction } from '@/hooks';
1416

1517
import { KEY_LIST_DOC } from '../api/useDocs';
1618
import { useRemoveDoc } from '../api/useRemoveDoc';
1719
import { useDocUtils } from '../hooks';
1820
import { Doc } from '../types';
1921

22+
const CANCEL_BUTTON_ID = 'modal-remove-doc-cancel-button';
23+
2024
interface ModalRemoveDocProps {
2125
doc: Doc;
2226
onClose: () => void;
@@ -57,32 +61,51 @@ export const ModalRemoveDoc = ({
5761
},
5862
});
5963

64+
useEffect(() => {
65+
const cancelButton = document.getElementById(CANCEL_BUTTON_ID);
66+
if (cancelButton instanceof HTMLButtonElement) {
67+
cancelButton.focus();
68+
}
69+
}, []);
70+
71+
const handleClose = () => {
72+
onClose();
73+
};
74+
75+
const handleDelete = () => {
76+
removeDoc({
77+
docId: doc.id,
78+
});
79+
};
80+
81+
const handleCloseKeyDown = useKeyboardAction(handleClose);
82+
const handleDeleteKeyDown = useKeyboardAction(handleDelete);
83+
6084
return (
6185
<Modal
6286
isOpen
6387
closeOnClickOutside
6488
hideCloseButton
65-
onClose={() => onClose()}
89+
onClose={handleClose}
6690
aria-describedby="modal-remove-doc-title"
6791
rightActions={
6892
<>
6993
<Button
94+
id={CANCEL_BUTTON_ID}
7095
aria-label={t('Cancel the deletion')}
7196
color="secondary"
7297
fullWidth
73-
onClick={() => onClose()}
98+
onClick={handleClose}
99+
onKeyDown={handleCloseKeyDown}
74100
>
75101
{t('Cancel')}
76102
</Button>
77103
<Button
78104
aria-label={t('Delete document')}
79105
color="danger"
80106
fullWidth
81-
onClick={() =>
82-
removeDoc({
83-
docId: doc.id,
84-
})
85-
}
107+
onClick={handleDelete}
108+
onKeyDown={handleDeleteKeyDown}
86109
>
87110
{t('Delete')}
88111
</Button>
@@ -108,7 +131,8 @@ export const ModalRemoveDoc = ({
108131
</Text>
109132
<ButtonCloseModal
110133
aria-label={t('Close the delete modal')}
111-
onClick={() => onClose()}
134+
onClick={handleClose}
135+
onKeyDown={handleCloseKeyDown}
112136
/>
113137
</Box>
114138
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './useKeyboardAction';
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { KeyboardEvent, useCallback } from 'react';
2+
3+
/**
4+
* Hook to handle keyboard actions (Enter and Space) on interactive elements.
5+
* Prevents default behavior and stops propagation to avoid conflicts.
6+
*
7+
* @param callback - The function to execute when Enter or Space is pressed
8+
* @returns A keyboard event handler function
9+
10+
*/
11+
export const useKeyboardAction = (callback: () => void) => {
12+
return useCallback(
13+
(event: KeyboardEvent<HTMLElement>) => {
14+
if (event.key === 'Enter' || event.key === ' ') {
15+
event.preventDefault();
16+
event.stopPropagation();
17+
callback();
18+
}
19+
},
20+
[callback],
21+
);
22+
};

0 commit comments

Comments
 (0)