diff --git a/CHANGELOG.md b/CHANGELOG.md index 0db1a7e381..15c9f786cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to - ♿(frontend) improve accessibility: - ♿(frontend) improve ARIA in doc grid and editor for a11y #1519 - ♿(frontend) improve accessibility and styling of summary table #1528 + - ♿(frontend) add focus trap and enter key support to remove doc modal #1531 - 🐛(docx) fix image overflow by limiting width to 600px during export #1525 - 🐛(frontend) preserve @ character when esc is pressed after typing it #1512 - 🐛(frontend) fix pdf embed to use full width #1526 diff --git a/src/frontend/apps/impress/src/components/dropdown-menu/DropdownMenu.tsx b/src/frontend/apps/impress/src/components/dropdown-menu/DropdownMenu.tsx index f0af724dd7..506a1b6bca 100644 --- a/src/frontend/apps/impress/src/components/dropdown-menu/DropdownMenu.tsx +++ b/src/frontend/apps/impress/src/components/dropdown-menu/DropdownMenu.tsx @@ -12,6 +12,7 @@ import { css } from 'styled-components'; import { Box, BoxButton, BoxProps, DropButton, Icon, Text } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; +import { useKeyboardAction } from '@/hooks'; import { useDropdownKeyboardNav } from './hook/useDropdownKeyboardNav'; @@ -57,6 +58,7 @@ export const DropdownMenu = ({ testId, }: PropsWithChildren) => { const { spacingsTokens, colorsTokens } = useCunninghamTheme(); + const keyboardAction = useKeyboardAction(); const [isOpen, setIsOpen] = useState(opened ?? false); const [focusedIndex, setFocusedIndex] = useState(-1); const blockButtonRef = useRef(null); @@ -93,6 +95,14 @@ export const DropdownMenu = ({ } }, [isOpen, options]); + const triggerOption = useCallback( + (option: DropdownMenuOption) => { + onOpenChange?.(false); + void option.callback?.(); + }, + [onOpenChange], + ); + if (disabled) { return children; } @@ -170,9 +180,9 @@ export const DropdownMenu = ({ onClick={(event) => { event.preventDefault(); event.stopPropagation(); - onOpenChange?.(false); - void option.callback?.(); + triggerOption(option); }} + onKeyDown={keyboardAction(() => triggerOption(option))} key={option.label} $align="center" $justify="space-between" diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalRemoveDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalRemoveDoc.tsx index a31fb07c9a..5edaf2ee3c 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalRemoveDoc.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalRemoveDoc.tsx @@ -1,16 +1,19 @@ import { Button, + ButtonElement, Modal, ModalSize, VariantType, useToastProvider, } from '@openfun/cunningham-react'; import { useRouter } from 'next/router'; +import { useEffect, useRef } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { Box, ButtonCloseModal, Text, TextErrors } from '@/components'; import { useConfig } from '@/core'; import { KEY_LIST_DOC_TRASHBIN } from '@/docs/docs-grid'; +import { useKeyboardAction } from '@/hooks'; import { KEY_LIST_DOC } from '../api/useDocs'; import { useRemoveDoc } from '../api/useRemoveDoc'; @@ -34,6 +37,7 @@ export const ModalRemoveDoc = ({ const trashBinCutoffDays = config?.TRASHBIN_CUTOFF_DAYS || 30; const { push } = useRouter(); const { hasChildren } = useDocUtils(doc); + const cancelButtonRef = useRef(null); const { mutate: removeDoc, isError, @@ -57,20 +61,47 @@ export const ModalRemoveDoc = ({ }, }); + useEffect(() => { + const TIMEOUT_MODAL_MOUNTING = 100; + const timeoutId = setTimeout(() => { + const buttonElement = cancelButtonRef.current; + if (buttonElement) { + buttonElement.focus(); + } + }, TIMEOUT_MODAL_MOUNTING); + + return () => clearTimeout(timeoutId); + }, []); + + const keyboardAction = useKeyboardAction(); + + const handleClose = () => { + onClose(); + }; + + const handleDelete = () => { + removeDoc({ docId: doc.id }); + }; + + const handleCloseKeyDown = keyboardAction(handleClose); + const handleDeleteKeyDown = keyboardAction(handleDelete); + return ( onClose()} + onClose={handleClose} aria-describedby="modal-remove-doc-title" rightActions={ <> @@ -78,11 +109,8 @@ export const ModalRemoveDoc = ({ aria-label={t('Delete document')} color="danger" fullWidth - onClick={() => - removeDoc({ - docId: doc.id, - }) - } + onClick={handleDelete} + onKeyDown={handleDeleteKeyDown} > {t('Delete')} @@ -108,7 +136,8 @@ export const ModalRemoveDoc = ({ onClose()} + onClick={handleClose} + onKeyDown={handleCloseKeyDown} /> } diff --git a/src/frontend/apps/impress/src/hooks/index.ts b/src/frontend/apps/impress/src/hooks/index.ts new file mode 100644 index 0000000000..90c70edb73 --- /dev/null +++ b/src/frontend/apps/impress/src/hooks/index.ts @@ -0,0 +1 @@ +export * from './useKeyboardAction'; diff --git a/src/frontend/apps/impress/src/hooks/useKeyboardAction.ts b/src/frontend/apps/impress/src/hooks/useKeyboardAction.ts new file mode 100644 index 0000000000..1c70d8482a --- /dev/null +++ b/src/frontend/apps/impress/src/hooks/useKeyboardAction.ts @@ -0,0 +1,22 @@ +import { KeyboardEvent, useCallback } from 'react'; + +type KeyboardActionCallback = () => void | Promise; +type KeyboardActionHandler = (event: KeyboardEvent) => void; + +/** + * Hook to create keyboard handlers that trigger the provided callback + * when the user presses Enter or Space. + */ +export const useKeyboardAction = () => { + return useCallback( + (callback: KeyboardActionCallback): KeyboardActionHandler => + (event: KeyboardEvent) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + event.stopPropagation(); + void callback(); + } + }, + [], + ); +};