Skip to content

Commit 6314cb3

Browse files
committed
✨(frontend) add focus trap and enter key support to remove doc modal
improves a11y by enabling keyboard-triggered modal with proper focus trap Signed-off-by: Cyril <[email protected]>
1 parent 3e410e3 commit 6314cb3

File tree

5 files changed

+73
-10
lines changed

5 files changed

+73
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to
1515
- ♿(frontend) improve accessibility:
1616
- ♿(frontend) improve ARIA in doc grid and editor for a11y #1519
1717
- ♿(frontend) improve accessibility and styling of summary table #1528
18+
- ♿(frontend) add focus trap and enter key support to remove doc modal #1531
1819
- 🐛(docx) fix image overflow by limiting width to 600px during export #1525
1920
- 🐛(frontend) preserve @ character when esc is pressed after typing it #1512
2021
- 🐛(frontend) fix pdf embed to use full width #1526

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { css } from 'styled-components';
1212

1313
import { Box, BoxButton, BoxProps, DropButton, Icon, Text } from '@/components';
1414
import { useCunninghamTheme } from '@/cunningham';
15+
import { useKeyboardAction } from '@/hooks';
1516

1617
import { useDropdownKeyboardNav } from './hook/useDropdownKeyboardNav';
1718

@@ -57,6 +58,7 @@ export const DropdownMenu = ({
5758
testId,
5859
}: PropsWithChildren<DropdownMenuProps>) => {
5960
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
61+
const keyboardAction = useKeyboardAction();
6062
const [isOpen, setIsOpen] = useState(opened ?? false);
6163
const [focusedIndex, setFocusedIndex] = useState(-1);
6264
const blockButtonRef = useRef<HTMLDivElement>(null);
@@ -93,6 +95,14 @@ export const DropdownMenu = ({
9395
}
9496
}, [isOpen, options]);
9597

98+
const triggerOption = useCallback(
99+
(option: DropdownMenuOption) => {
100+
onOpenChange?.(false);
101+
void option.callback?.();
102+
},
103+
[onOpenChange],
104+
);
105+
96106
if (disabled) {
97107
return children;
98108
}
@@ -170,9 +180,9 @@ export const DropdownMenu = ({
170180
onClick={(event) => {
171181
event.preventDefault();
172182
event.stopPropagation();
173-
onOpenChange?.(false);
174-
void option.callback?.();
183+
triggerOption(option);
175184
}}
185+
onKeyDown={keyboardAction(() => triggerOption(option))}
176186
key={option.label}
177187
$align="center"
178188
$justify="space-between"

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

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import {
22
Button,
3+
ButtonElement,
34
Modal,
45
ModalSize,
56
VariantType,
67
useToastProvider,
78
} from '@openfun/cunningham-react';
89
import { useRouter } from 'next/router';
10+
import { useEffect, useRef } from 'react';
911
import { Trans, useTranslation } from 'react-i18next';
1012

1113
import { Box, ButtonCloseModal, Text, TextErrors } from '@/components';
1214
import { useConfig } from '@/core';
1315
import { KEY_LIST_DOC_TRASHBIN } from '@/docs/docs-grid';
16+
import { useKeyboardAction } from '@/hooks';
1417

1518
import { KEY_LIST_DOC } from '../api/useDocs';
1619
import { useRemoveDoc } from '../api/useRemoveDoc';
@@ -34,6 +37,7 @@ export const ModalRemoveDoc = ({
3437
const trashBinCutoffDays = config?.TRASHBIN_CUTOFF_DAYS || 30;
3538
const { push } = useRouter();
3639
const { hasChildren } = useDocUtils(doc);
40+
const cancelButtonRef = useRef<ButtonElement>(null);
3741
const {
3842
mutate: removeDoc,
3943
isError,
@@ -57,32 +61,56 @@ export const ModalRemoveDoc = ({
5761
},
5862
});
5963

64+
useEffect(() => {
65+
const TIMEOUT_MODAL_MOUNTING = 100;
66+
const timeoutId = setTimeout(() => {
67+
const buttonElement = cancelButtonRef.current;
68+
if (buttonElement) {
69+
buttonElement.focus();
70+
}
71+
}, TIMEOUT_MODAL_MOUNTING);
72+
73+
return () => clearTimeout(timeoutId);
74+
}, []);
75+
76+
const keyboardAction = useKeyboardAction();
77+
78+
const handleClose = () => {
79+
onClose();
80+
};
81+
82+
const handleDelete = () => {
83+
removeDoc({ docId: doc.id });
84+
};
85+
86+
const handleCloseKeyDown = keyboardAction(handleClose);
87+
const handleDeleteKeyDown = keyboardAction(handleDelete);
88+
6089
return (
6190
<Modal
6291
isOpen
6392
closeOnClickOutside
6493
hideCloseButton
65-
onClose={() => onClose()}
94+
onClose={handleClose}
6695
aria-describedby="modal-remove-doc-title"
6796
rightActions={
6897
<>
6998
<Button
99+
ref={cancelButtonRef}
70100
aria-label={t('Cancel the deletion')}
71101
color="secondary"
72102
fullWidth
73-
onClick={() => onClose()}
103+
onClick={handleClose}
104+
onKeyDown={handleCloseKeyDown}
74105
>
75106
{t('Cancel')}
76107
</Button>
77108
<Button
78109
aria-label={t('Delete document')}
79110
color="danger"
80111
fullWidth
81-
onClick={() =>
82-
removeDoc({
83-
docId: doc.id,
84-
})
85-
}
112+
onClick={handleDelete}
113+
onKeyDown={handleDeleteKeyDown}
86114
>
87115
{t('Delete')}
88116
</Button>
@@ -108,7 +136,8 @@ export const ModalRemoveDoc = ({
108136
</Text>
109137
<ButtonCloseModal
110138
aria-label={t('Close the delete modal')}
111-
onClick={() => onClose()}
139+
onClick={handleClose}
140+
onKeyDown={handleCloseKeyDown}
112141
/>
113142
</Box>
114143
}
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+
type KeyboardActionCallback = () => void | Promise<unknown>;
4+
type KeyboardActionHandler = (event: KeyboardEvent<HTMLElement>) => void;
5+
6+
/**
7+
* Hook to create keyboard handlers that trigger the provided callback
8+
* when the user presses Enter or Space.
9+
*/
10+
export const useKeyboardAction = () => {
11+
return useCallback(
12+
(callback: KeyboardActionCallback): KeyboardActionHandler =>
13+
(event: KeyboardEvent<HTMLElement>) => {
14+
if (event.key === 'Enter' || event.key === ' ') {
15+
event.preventDefault();
16+
event.stopPropagation();
17+
void callback();
18+
}
19+
},
20+
[],
21+
);
22+
};

0 commit comments

Comments
 (0)