nicolium: make hotkey modal more screen reader-readable

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-03-05 12:09:09 +01:00
parent b56278144c
commit e1dc1828e2
2 changed files with 117 additions and 74 deletions

View File

@ -1282,6 +1282,7 @@
"join_event.request_success": "Requested to join the event",
"join_event.success": "Joined the event",
"join_event.title": "Join event",
"keyboard_shortcuts.action": "Action",
"keyboard_shortcuts.back": "to navigate back",
"keyboard_shortcuts.blocked": "to open blocked users list",
"keyboard_shortcuts.boost": "to repost",
@ -1293,6 +1294,17 @@
"keyboard_shortcuts.heading": "Keyboard shortcuts",
"keyboard_shortcuts.home": "to open home timeline",
"keyboard_shortcuts.hotkey": "Hotkey",
"keyboard_shortcuts.joiners.or": "or",
"keyboard_shortcuts.joiners.plus": "plus",
"keyboard_shortcuts.joiners.then": "then",
"keyboard_shortcuts.key_names.alt": "Alt",
"keyboard_shortcuts.key_names.backspace": "Backspace",
"keyboard_shortcuts.key_names.down": "Arrow down",
"keyboard_shortcuts.key_names.enter": "Enter",
"keyboard_shortcuts.key_names.esc": "Escape",
"keyboard_shortcuts.key_names.question_mark": "Question mark",
"keyboard_shortcuts.key_names.slash": "Slash",
"keyboard_shortcuts.key_names.up": "Arrow up",
"keyboard_shortcuts.legend": "to display this legend",
"keyboard_shortcuts.mention": "to mention author",
"keyboard_shortcuts.muted": "to open muted users list",

View File

@ -1,6 +1,6 @@
import clsx from 'clsx';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { defineMessages, FormattedMessage, type MessageDescriptor, useIntl } from 'react-intl';
import Modal from '@/components/ui/modal';
import { useFeatures } from '@/hooks/use-features';
@ -8,12 +8,88 @@ import { useLoggedIn } from '@/hooks/use-logged-in';
import type { BaseModalProps } from '@/features/ui/components/modal-root';
const messages = defineMessages({
keyNameSlash: { id: 'keyboard_shortcuts.key_names.slash', defaultMessage: 'Slash' },
keyNameQuestionMark: {
id: 'keyboard_shortcuts.key_names.question_mark',
defaultMessage: 'Question mark',
},
keyNameAlt: { id: 'keyboard_shortcuts.key_names.alt', defaultMessage: 'Alt' },
keyNameBackspace: { id: 'keyboard_shortcuts.key_names.backspace', defaultMessage: 'Backspace' },
keyNameDown: { id: 'keyboard_shortcuts.key_names.down', defaultMessage: 'Arrow down' },
keyNameEnter: { id: 'keyboard_shortcuts.key_names.enter', defaultMessage: 'Enter' },
keyNameEsc: { id: 'keyboard_shortcuts.key_names.esc', defaultMessage: 'Escape' },
keyNameUp: { id: 'keyboard_shortcuts.key_names.up', defaultMessage: 'Arrow up' },
joinerOr: { id: 'keyboard_shortcuts.joiners.or', defaultMessage: 'or' },
joinerPlus: { id: 'keyboard_shortcuts.joiners.plus', defaultMessage: 'plus' },
joinerThen: { id: 'keyboard_shortcuts.joiners.then', defaultMessage: 'then' },
});
const Hotkey: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<kbd className='rounded-md border border-solid border-primary-200 bg-primary-50 px-1.5 py-1 font-sans text-xs dark:border-gray-700 dark:bg-gray-800'>
{children}
</kbd>
);
const spokenKeyNames: Record<string, MessageDescriptor> = {
'/': messages.keyNameSlash,
'?': messages.keyNameQuestionMark,
alt: messages.keyNameAlt,
backspace: messages.keyNameBackspace,
down: messages.keyNameDown,
enter: messages.keyNameEnter,
esc: messages.keyNameEsc,
up: messages.keyNameUp,
};
const getSpokenKeyName = (keyName: string) => {
if (spokenKeyNames[keyName]) return spokenKeyNames[keyName];
if (/^[a-z]$/i.test(keyName)) return keyName.toUpperCase();
return keyName;
};
type KeyJoiner = 'or' | 'plus' | 'then';
const visualJoiners: Record<KeyJoiner, string> = {
or: ', ',
plus: ' + ',
then: ' + ',
};
const spokenJoiners: Record<KeyJoiner, MessageDescriptor> = {
or: messages.joinerOr,
plus: messages.joinerPlus,
then: messages.joinerThen,
};
const HotkeyBinding: React.FC<{ keys: string[]; joiner?: KeyJoiner }> = ({
keys,
joiner = 'or',
}) => {
const intl = useIntl();
const spokenBinding = keys
.map((keyName) => {
const spokenKey = getSpokenKeyName(keyName);
return typeof spokenKey === 'string' ? spokenKey : intl.formatMessage(spokenKey);
})
.join(` ${intl.formatMessage(spokenJoiners[joiner])} `);
return (
<span>
<span aria-hidden='true'>
{keys.map((keyName, idx) => (
<React.Fragment key={keyName}>
{idx > 0 && visualJoiners[joiner]}
<Hotkey>{keyName}</Hotkey>
</React.Fragment>
))}
</span>
<span className='sr-only'>{spokenBinding}</span>
</span>
);
};
const TableCell: React.FC<{ className?: string; children: React.ReactNode }> = ({
className,
children,
@ -41,17 +117,17 @@ const HotkeysModal: React.FC<BaseModalProps> = ({ onClose }) => {
const hotkeys = [
isLoggedIn && {
key: <Hotkey>r</Hotkey>,
key: <HotkeyBinding keys={['r']} />,
label: <FormattedMessage id='keyboard_shortcuts.reply' defaultMessage='to reply' />,
},
isLoggedIn && {
key: <Hotkey>m</Hotkey>,
key: <HotkeyBinding keys={['m']} />,
label: (
<FormattedMessage id='keyboard_shortcuts.mention' defaultMessage='to mention author' />
),
},
{
key: <Hotkey>p</Hotkey>,
key: <HotkeyBinding keys={['p']} />,
label: (
<FormattedMessage
id='keyboard_shortcuts.profile'
@ -60,32 +136,28 @@ const HotkeysModal: React.FC<BaseModalProps> = ({ onClose }) => {
),
},
isLoggedIn && {
key: <Hotkey>f</Hotkey>,
key: <HotkeyBinding keys={['f']} />,
label: <FormattedMessage id='keyboard_shortcuts.favourite' defaultMessage='to like' />,
},
isLoggedIn &&
features.emojiReacts && {
key: <Hotkey>e</Hotkey>,
key: <HotkeyBinding keys={['e']} />,
label: <FormattedMessage id='keyboard_shortcuts.react' defaultMessage='to react' />,
},
isLoggedIn && {
key: <Hotkey>b</Hotkey>,
key: <HotkeyBinding keys={['b']} />,
label: <FormattedMessage id='keyboard_shortcuts.boost' defaultMessage='to repost' />,
},
{
key: (
<>
<Hotkey>enter</Hotkey>, <Hotkey>o</Hotkey>
</>
),
key: <HotkeyBinding keys={['enter', 'o']} joiner='or' />,
label: <FormattedMessage id='keyboard_shortcuts.enter' defaultMessage='to open post' />,
},
{
key: <Hotkey>a</Hotkey>,
key: <HotkeyBinding keys={['a']} />,
label: <FormattedMessage id='keyboard_shortcuts.open_media' defaultMessage='to open media' />,
},
features.spoilers && {
key: <Hotkey>x</Hotkey>,
key: <HotkeyBinding keys={['x']} />,
label: (
<FormattedMessage
id='keyboard_shortcuts.toggle_hidden'
@ -94,7 +166,7 @@ const HotkeysModal: React.FC<BaseModalProps> = ({ onClose }) => {
),
},
features.spoilers && {
key: <Hotkey>h</Hotkey>,
key: <HotkeyBinding keys={['h']} />,
label: (
<FormattedMessage
id='keyboard_shortcuts.toggle_sensitivity'
@ -103,27 +175,19 @@ const HotkeysModal: React.FC<BaseModalProps> = ({ onClose }) => {
),
},
{
key: (
<>
<Hotkey>up</Hotkey>, <Hotkey>k</Hotkey>
</>
),
key: <HotkeyBinding keys={['up', 'k']} joiner='or' />,
label: (
<FormattedMessage id='keyboard_shortcuts.up' defaultMessage='to move up in the list' />
),
},
{
key: (
<>
<Hotkey>down</Hotkey>, <Hotkey>j</Hotkey>
</>
),
key: <HotkeyBinding keys={['down', 'j']} joiner='or' />,
label: (
<FormattedMessage id='keyboard_shortcuts.down' defaultMessage='to move down in the list' />
),
},
isLoggedIn && {
key: <Hotkey>n</Hotkey>,
key: <HotkeyBinding keys={['n']} />,
label: (
<FormattedMessage
id='keyboard_shortcuts.compose'
@ -132,23 +196,15 @@ const HotkeysModal: React.FC<BaseModalProps> = ({ onClose }) => {
),
},
isLoggedIn && {
key: (
<>
<Hotkey>alt</Hotkey> + <Hotkey>n</Hotkey>
</>
),
key: <HotkeyBinding keys={['alt', 'n']} joiner='plus' />,
label: <FormattedMessage id='keyboard_shortcuts.toot' defaultMessage='to start a new post' />,
},
{
key: <Hotkey>backspace</Hotkey>,
key: <HotkeyBinding keys={['backspace']} />,
label: <FormattedMessage id='keyboard_shortcuts.back' defaultMessage='to navigate back' />,
},
isLoggedIn && {
key: (
<>
<Hotkey>s</Hotkey>, <Hotkey>/</Hotkey>
</>
),
key: <HotkeyBinding keys={['s', '/']} joiner='or' />,
label: document.querySelector('#search') ? (
<FormattedMessage id='keyboard_shortcuts.search' defaultMessage='to focus search' />
) : (
@ -159,7 +215,7 @@ const HotkeysModal: React.FC<BaseModalProps> = ({ onClose }) => {
),
},
{
key: <Hotkey>esc</Hotkey>,
key: <HotkeyBinding keys={['esc']} />,
label: (
<FormattedMessage
id='keyboard_shortcuts.unfocus'
@ -168,21 +224,13 @@ const HotkeysModal: React.FC<BaseModalProps> = ({ onClose }) => {
),
},
isLoggedIn && {
key: (
<>
<Hotkey>g</Hotkey> + <Hotkey>h</Hotkey>
</>
),
key: <HotkeyBinding keys={['g', 'h']} joiner='then' />,
label: (
<FormattedMessage id='keyboard_shortcuts.home' defaultMessage='to open home timeline' />
),
},
isLoggedIn && {
key: (
<>
<Hotkey>g</Hotkey> + <Hotkey>n</Hotkey>
</>
),
key: <HotkeyBinding keys={['g', 'n']} joiner='then' />,
label: (
<FormattedMessage
id='keyboard_shortcuts.notifications'
@ -191,21 +239,13 @@ const HotkeysModal: React.FC<BaseModalProps> = ({ onClose }) => {
),
},
isLoggedIn && {
key: (
<>
<Hotkey>g</Hotkey> + <Hotkey>f</Hotkey>
</>
),
key: <HotkeyBinding keys={['g', 'f']} joiner='then' />,
label: (
<FormattedMessage id='keyboard_shortcuts.favourites' defaultMessage='to open likes list' />
),
},
isLoggedIn && {
key: (
<>
<Hotkey>g</Hotkey> + <Hotkey>u</Hotkey>
</>
),
key: <HotkeyBinding keys={['g', 'u']} joiner='then' />,
label: (
<FormattedMessage
id='keyboard_shortcuts.my_profile'
@ -214,11 +254,7 @@ const HotkeysModal: React.FC<BaseModalProps> = ({ onClose }) => {
),
},
isLoggedIn && {
key: (
<>
<Hotkey>g</Hotkey> + <Hotkey>b</Hotkey>
</>
),
key: <HotkeyBinding keys={['g', 'b']} joiner='then' />,
label: (
<FormattedMessage
id='keyboard_shortcuts.blocked'
@ -227,22 +263,14 @@ const HotkeysModal: React.FC<BaseModalProps> = ({ onClose }) => {
),
},
isLoggedIn && {
key: (
<>
<Hotkey>g</Hotkey> + <Hotkey>m</Hotkey>
</>
),
key: <HotkeyBinding keys={['g', 'm']} joiner='then' />,
label: (
<FormattedMessage id='keyboard_shortcuts.muted' defaultMessage='to open muted users list' />
),
},
isLoggedIn &&
features.followRequests && {
key: (
<>
<Hotkey>g</Hotkey> + <Hotkey>r</Hotkey>
</>
),
key: <HotkeyBinding keys={['g', 'r']} joiner='then' />,
label: (
<FormattedMessage
id='keyboard_shortcuts.requests'
@ -251,7 +279,7 @@ const HotkeysModal: React.FC<BaseModalProps> = ({ onClose }) => {
),
},
{
key: <Hotkey>?</Hotkey>,
key: <HotkeyBinding keys={['?']} />,
label: (
<FormattedMessage id='keyboard_shortcuts.legend' defaultMessage='to display this legend' />
),
@ -291,6 +319,9 @@ const HotkeysModal: React.FC<BaseModalProps> = ({ onClose }) => {
<th className='pb-2 font-bold'>
<FormattedMessage id='keyboard_shortcuts.hotkey' defaultMessage='Hotkey' />
</th>
<th className='pb-2 font-bold'>
<FormattedMessage id='keyboard_shortcuts.action' defaultMessage='Action' />
</th>
</tr>
</thead>
<tbody>