✨(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 <c.gromoff@gmail.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<DropdownMenuProps>) => {
|
||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||
const keyboardAction = useKeyboardAction();
|
||||
const [isOpen, setIsOpen] = useState(opened ?? false);
|
||||
const [focusedIndex, setFocusedIndex] = useState(-1);
|
||||
const blockButtonRef = useRef<HTMLDivElement>(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"
|
||||
|
||||
@@ -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<ButtonElement>(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 (
|
||||
<Modal
|
||||
isOpen
|
||||
closeOnClickOutside
|
||||
hideCloseButton
|
||||
onClose={() => onClose()}
|
||||
onClose={handleClose}
|
||||
aria-describedby="modal-remove-doc-title"
|
||||
rightActions={
|
||||
<>
|
||||
<Button
|
||||
ref={cancelButtonRef}
|
||||
aria-label={t('Cancel the deletion')}
|
||||
color="secondary"
|
||||
fullWidth
|
||||
onClick={() => onClose()}
|
||||
onClick={handleClose}
|
||||
onKeyDown={handleCloseKeyDown}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
@@ -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')}
|
||||
</Button>
|
||||
@@ -108,7 +136,8 @@ export const ModalRemoveDoc = ({
|
||||
</Text>
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close the delete modal')}
|
||||
onClick={() => onClose()}
|
||||
onClick={handleClose}
|
||||
onKeyDown={handleCloseKeyDown}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
|
||||
1
src/frontend/apps/impress/src/hooks/index.ts
Normal file
1
src/frontend/apps/impress/src/hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './useKeyboardAction';
|
||||
22
src/frontend/apps/impress/src/hooks/useKeyboardAction.ts
Normal file
22
src/frontend/apps/impress/src/hooks/useKeyboardAction.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { KeyboardEvent, useCallback } from 'react';
|
||||
|
||||
type KeyboardActionCallback = () => void | Promise<unknown>;
|
||||
type KeyboardActionHandler = (event: KeyboardEvent<HTMLElement>) => 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<HTMLElement>) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void callback();
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user