diff --git a/CHANGELOG.md b/CHANGELOG.md index 4941df6f..fa5a74ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to - ♿(frontend) focus skip link on headings and skip grid dropzone #1983 - ♿️(frontend) fix search modal accessibility issues #2054 - ♿️(frontend) add sr-only format to export download button #2088 +- ♿️(frontend) announce formatting shortcuts for screen readers #2070 ## [v4.8.2] - 2026-03-19 diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useShortcuts.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useShortcuts.tsx index 09c8625d..229ce9f7 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useShortcuts.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useShortcuts.tsx @@ -1,11 +1,83 @@ -import { useEffect } from 'react'; +import { announce } from '@react-aria/live-announcer'; +import { useCallback, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { DocsBlockNoteEditor } from '../types'; +const getFormattingShortcutLabel = ( + event: KeyboardEvent, + t: (key: string) => string, +): string | null => { + const isMod = event.ctrlKey || event.metaKey; + if (!isMod) { + return null; + } + + if (event.altKey) { + switch (event.code) { + case 'Digit1': + return t('Heading 1 applied'); + case 'Digit2': + return t('Heading 2 applied'); + case 'Digit3': + return t('Heading 3 applied'); + default: + return null; + } + } + + if (event.shiftKey) { + switch (event.code) { + case 'Digit0': + return t('Paragraph applied'); + case 'Digit6': + return t('Toggle list applied'); + case 'Digit7': + return t('Numbered list applied'); + case 'Digit8': + return t('Bulleted list applied'); + case 'Digit9': + return t('Checklist applied'); + case 'KeyC': + return t('Code block applied'); + default: + return null; + } + } + + return null; +}; + export const useShortcuts = ( editor: DocsBlockNoteEditor, el: HTMLDivElement | null, ) => { + const { t } = useTranslation(); + + const handleFormattingShortcut = useCallback( + (event: KeyboardEvent) => { + if (!editor?.isFocused()) { + return; + } + + const label = getFormattingShortcutLabel(event, t); + if (label) { + setTimeout(() => { + announce(label, 'assertive'); + }, 150); + } + }, + [editor, t], + ); + + useEffect(() => { + el?.addEventListener('keydown', handleFormattingShortcut, true); + + return () => { + el?.removeEventListener('keydown', handleFormattingShortcut, true); + }; + }, [el, handleFormattingShortcut]); + useEffect(() => { // Check if editor and its view are mounted if (!editor || !el) {