From aadabf8d3c1aaf95a5869f66cc3ce910003c941f Mon Sep 17 00:00:00 2001 From: Cyril Date: Wed, 18 Mar 2026 13:29:19 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BF=EF=B8=8F(frontend)=20announce=20forma?= =?UTF-8?q?tting=20shortcuts=20for=20screen=20readers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Announce formatting shortcuts (headings, lists, paragraph, code block). --- CHANGELOG.md | 1 + .../docs/doc-editor/hook/useShortcuts.tsx | 74 ++++++++++++++++++- 2 files changed, 74 insertions(+), 1 deletion(-) 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) {