From 4969ef9b6d670c967690d914a1f7c3b9021f4408 Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Sat, 17 Oct 2026 23:43:49 -0400 Subject: [PATCH] fix(a11y): the keyboard shortcuts dialog lists every shortcut (#334) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ? dialog showed 7 shortcuts and left out Ctrl+K, ↑ to edit, all composer formatting keys and the push-to-talk / deafen keys. Add them: formatting rows are generated from the editor's INLINE_HOTKEYS / BLOCK_HOTKEYS so they can't drift, and the call keys show the user's own bindings. Fixes #334 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5 --- .../shortcuts/KeyboardShortcutsDialog.tsx | 41 +++++++++++++++++++ .../features/shortcuts/shortcutLabels.test.ts | 16 ++++++++ src/app/features/shortcuts/shortcutLabels.ts | 29 +++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 src/app/features/shortcuts/shortcutLabels.test.ts create mode 100644 src/app/features/shortcuts/shortcutLabels.ts diff --git a/src/app/features/shortcuts/KeyboardShortcutsDialog.tsx b/src/app/features/shortcuts/KeyboardShortcutsDialog.tsx index 7fdb21679..c17c42257 100644 --- a/src/app/features/shortcuts/KeyboardShortcutsDialog.tsx +++ b/src/app/features/shortcuts/KeyboardShortcutsDialog.tsx @@ -25,6 +25,9 @@ import { settingsAtom } from '../../state/settings'; import { isMacOS } from '../../utils/user-agent'; import { KeySymbol } from '../../utils/key-symbol'; import * as css from './KeyboardShortcutsDialog.css'; +import { BLOCK_HOTKEYS, INLINE_HOTKEYS } from '../../components/editor/keyboard'; +import { MarkType } from '../../components/editor/types'; +import { BLOCK_LABELS, MARK_LABELS, hotkeyKeys, keyCodeLabel } from './shortcutLabels'; /** Global open-state for the keyboard shortcuts help dialog. */ export const keyboardShortcutsDialogAtom = atom(false); @@ -98,6 +101,9 @@ export function KeyboardShortcutsDialog() { const { open, closeDialog } = useKeyboardShortcutsDialog(); const modalStyle = useModalStyle(480); const [enterForNewline] = useSetting(settingsAtom, 'enterForNewline'); + const [pttMode] = useSetting(settingsAtom, 'pttMode'); + const [pttKey] = useSetting(settingsAtom, 'pttKey'); + const [deafenKey] = useSetting(settingsAtom, 'deafenKey'); const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl'; @@ -109,6 +115,8 @@ export function KeyboardShortcutsDialog() { rows: [ { description: 'Show keyboard shortcuts', keys: ['?'] }, { description: 'Close open panel, otherwise mark room as read', keys: [KeySymbol.Escape] }, + // [Gitea #334] the quick switcher / search was missing + { description: 'Quick switcher: jump to a room or person', keys: [modKey, 'K'] }, ], }, { @@ -124,6 +132,26 @@ export function KeyboardShortcutsDialog() { keys: enterForNewline ? ['Enter'] : [KeySymbol.Shift, 'Enter'], }, { description: 'Send message (always)', keys: [modKey, 'Enter'] }, + { description: 'Edit your last message (empty composer)', keys: ['↑'] }, + ], + }, + { + // [Gitea #334] generated from the editor's own bindings so they can't drift + title: 'Composer formatting', + rows: [ + ...Object.entries(INLINE_HOTKEYS).map(([hotkey, mark]) => ({ + description: + mark === MarkType.StrikeThrough + ? `${MARK_LABELS[mark]} (not the browser's Save)` + : MARK_LABELS[mark], + keys: hotkeyKeys(hotkey, modKey), + })), + { description: 'Heading 1 / 2 / 3', keys: [modKey, '1 / 2 / 3'] }, + ...Object.entries(BLOCK_HOTKEYS).map(([hotkey, block]) => ({ + description: BLOCK_LABELS[block] ?? block, + keys: hotkeyKeys(hotkey, modKey), + })), + { description: 'Clear formatting', keys: [modKey, 'E'] }, ], }, { @@ -132,6 +160,19 @@ export function KeyboardShortcutsDialog() { { description: 'Reveal message actions (react, reply, more)', keys: ['Hover / focus'] }, ], }, + { + // [Gitea #334] the user's own bindings (Settings → Calls) + title: 'Calls', + rows: [ + { + description: pttMode + ? 'Push to talk (hold)' + : 'Push to talk (hold; turn on in Settings → Calls)', + keys: [keyCodeLabel(pttKey)], + }, + { description: 'Deafen / undeafen', keys: [keyCodeLabel(deafenKey)] }, + ], + }, ]; return ( diff --git a/src/app/features/shortcuts/shortcutLabels.test.ts b/src/app/features/shortcuts/shortcutLabels.test.ts new file mode 100644 index 000000000..2d7e9bdb7 --- /dev/null +++ b/src/app/features/shortcuts/shortcutLabels.test.ts @@ -0,0 +1,16 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { hotkeyKeys, keyCodeLabel } from './shortcutLabels'; + +test('hotkeyKeys (#334)', () => { + assert.deepEqual(hotkeyKeys('mod+b', 'Ctrl'), ['Ctrl', 'B']); + assert.deepEqual(hotkeyKeys("mod+'", '⌘'), ['⌘', "'"]); + assert.deepEqual(hotkeyKeys('mod+[', 'Ctrl'), ['Ctrl', '[']); +}); + +test('keyCodeLabel (#334)', () => { + assert.equal(keyCodeLabel('KeyM'), 'M'); + assert.equal(keyCodeLabel('Digit1'), '1'); + assert.equal(keyCodeLabel('Space'), 'Space'); + assert.equal(keyCodeLabel(''), 'Not set'); +}); diff --git a/src/app/features/shortcuts/shortcutLabels.ts b/src/app/features/shortcuts/shortcutLabels.ts new file mode 100644 index 000000000..5f95fe32b --- /dev/null +++ b/src/app/features/shortcuts/shortcutLabels.ts @@ -0,0 +1,29 @@ +import { BlockType, MarkType } from '../../components/editor/types'; + +/** Pure: an is-hotkey string ('mod+b', "mod+'") as keycap labels. */ +export const hotkeyKeys = (hotkey: string, modKey: string): string[] => + hotkey.split('+').map((k) => (k === 'mod' ? modKey : k.length === 1 ? k.toUpperCase() : k)); + +/** Pure: a KeyboardEvent.code ('KeyM', 'Digit1', 'Space') as a keycap label. */ +export const keyCodeLabel = (code: string | undefined): string => { + if (!code) return 'Not set'; + if (/^Key[A-Z]$/.test(code)) return code.slice(3); + if (/^Digit\d$/.test(code)) return code.slice(5); + return code; +}; + +export const MARK_LABELS: Record = { + [MarkType.Bold]: 'Bold', + [MarkType.Italic]: 'Italic', + [MarkType.Underline]: 'Underline', + [MarkType.StrikeThrough]: 'Strikethrough', + [MarkType.Code]: 'Inline code', + [MarkType.Spoiler]: 'Spoiler', +}; + +export const BLOCK_LABELS: Partial> = { + [BlockType.OrderedList]: 'Numbered list', + [BlockType.UnorderedList]: 'Bulleted list', + [BlockType.BlockQuote]: 'Quote', + [BlockType.CodeBlock]: 'Code block', +};