1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +02:00

Desktop: Accessiblity: Make keyboard shortcuts settings screen keyboard-navigable (#11232)

This commit is contained in:
Henry Heino 2024-10-26 13:06:18 -07:00 committed by GitHub
parent f07e4e9b5a
commit 4057aae300
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 93 additions and 32 deletions

View File

@ -9,8 +9,8 @@ import useCommandStatus from './utils/useCommandStatus';
import styles_ from './styles'; import styles_ from './styles';
import { _ } from '@joplin/lib/locale'; import { _ } from '@joplin/lib/locale';
const bridge = require('@electron/remote').require('./bridge').default;
import shim from '@joplin/lib/shim'; import shim from '@joplin/lib/shim';
import bridge from '../../services/bridge';
const keymapService = KeymapService.instance(); const keymapService = KeymapService.instance();
@ -25,7 +25,6 @@ export const KeymapConfigScreen = ({ themeId }: KeymapConfigScreenProps) => {
const [keymapItems, keymapError, overrideKeymapItems, setAccelerator, resetAccelerator] = useKeymap(); const [keymapItems, keymapError, overrideKeymapItems, setAccelerator, resetAccelerator] = useKeymap();
const [recorderError, setRecorderError] = useState<Error>(null); const [recorderError, setRecorderError] = useState<Error>(null);
const [editing, enableEditing, disableEditing] = useCommandStatus(); const [editing, enableEditing, disableEditing] = useCommandStatus();
const [hovering, enableHovering, disableHovering] = useCommandStatus();
const handleSave = (event: { commandName: string; accelerator: string }) => { const handleSave = (event: { commandName: string; accelerator: string }) => {
const { commandName, accelerator } = event; const { commandName, accelerator } = event;
@ -95,13 +94,14 @@ export const KeymapConfigScreen = ({ themeId }: KeymapConfigScreenProps) => {
}; };
const renderStatus = (commandName: string) => { const renderStatus = (commandName: string) => {
if (editing[commandName]) { if (!editing[commandName]) {
return (recorderError && <i className="fa fa-exclamation-triangle" title={recorderError.message} />); const editLabel = _('Change shortcut for "%s"', getLabel(commandName));
} else if (hovering[commandName]) { return <i className="fa fa-pen" role='img' aria-label={editLabel} title={editLabel}/>;
return (<i className="fa fa-pen" />); } else if (recorderError) {
} else { return <i className="fa fa-exclamation-triangle" role='img' aria-label={recorderError.message} title={recorderError.message} />;
return null;
} }
return null;
}; };
const renderError = (error: Error) => { const renderError = (error: Error) => {
@ -117,11 +117,16 @@ export const KeymapConfigScreen = ({ themeId }: KeymapConfigScreenProps) => {
}; };
const renderKeymapRow = ({ command, accelerator }: KeymapItem) => { const renderKeymapRow = ({ command, accelerator }: KeymapItem) => {
const handleClick = () => enableEditing(command); const handleClick = () => {
const handleMouseEnter = () => enableHovering(command); if (!editing[command]) {
const handleMouseLeave = () => disableHovering(command); enableEditing(command);
} else if (recorderError) {
void bridge().showErrorMessageBox(recorderError.message);
}
};
const statusContent = renderStatus(command);
const cellContent = const cellContent =
<div style={styles.tableCell} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}> <div className='keymap-shortcut-row-content'>
{editing[command] ? {editing[command] ?
<ShortcutRecorder <ShortcutRecorder
onSave={handleSave} onSave={handleSave}
@ -139,9 +144,15 @@ export const KeymapConfigScreen = ({ themeId }: KeymapConfigScreenProps) => {
} }
</div> </div>
} }
<div style={styles.tableCellStatus} onClick={handleClick}> <button
{renderStatus(command)} className={`flat-button edit ${editing[command] ? '-editing' : ''}`}
</div> style={styles.tableCellStatus}
aria-live={recorderError ? 'polite' : null}
tabIndex={statusContent ? 0 : -1}
onClick={handleClick}
>
{statusContent}
</button>
</div>; </div>;
return ( return (

View File

@ -43,6 +43,12 @@ export const ShortcutRecorder = ({ onSave, onReset, onCancel, onError, initialAc
}, [accelerator]); }, [accelerator]);
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => { const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
// Shift-tab and tab are needed for navigating the shortcuts screen with the keyboard. Do not
// .preventDefault.
if (event.code === 'Tab' && !event.metaKey && !event.altKey && !event.ctrlKey) {
return;
}
event.preventDefault(); event.preventDefault();
const newAccelerator = keymapService.domToElectronAccelerator(event); const newAccelerator = keymapService.domToElectronAccelerator(event);
@ -60,14 +66,25 @@ export const ShortcutRecorder = ({ onSave, onReset, onCancel, onError, initialAc
} }
}; };
const hintText = _('Press the shortcut and then press ENTER. Or, press BACKSPACE to clear the shortcut.');
const placeholderText = _('Press the shortcut');
return ( return (
<div style={styles.recorderContainer}> <div className='shortcut-recorder' style={styles.recorderContainer}>
<input <input
className='shortcut text-input'
value={accelerator} value={accelerator}
placeholder={_('Press the shortcut')} aria-label={accelerator ? accelerator : placeholderText}
placeholder={placeholderText}
title={hintText}
aria-description={hintText}
aria-invalid={accelerator && !saveAllowed}
// With readOnly, aria-live polite seems necessary for screen readers to read
// the shortcut as it updates.
aria-live='polite'
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
style={styles.recorderInput}
title={_('Press the shortcut and then press ENTER. Or, press BACKSPACE to clear the shortcut.')}
readOnly readOnly
autoFocus autoFocus
/> />

View File

@ -0,0 +1,2 @@
@use "./styles/keymap-shortcut-row-content.scss";
@use "./styles/shortcut-recorder.scss";

View File

@ -14,21 +14,12 @@ export default function styles(themeId: number) {
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
}, },
recorderContainer: {
padding: 2,
flexGrow: 1,
},
filterInput: { filterInput: {
...theme.inputStyle, ...theme.inputStyle,
flexGrow: 1, flexGrow: 1,
minHeight: 29, minHeight: 29,
alignSelf: 'center', alignSelf: 'center',
}, },
recorderInput: {
...theme.inputStyle,
minHeight: 29,
width: '200px',
},
label: { label: {
...theme.textStyle, ...theme.textStyle,
alignSelf: 'center', alignSelf: 'center',
@ -48,10 +39,6 @@ export default function styles(themeId: number) {
...theme.textStyle, ...theme.textStyle,
width: 'auto', width: 'auto',
}, },
tableCell: {
display: 'flex',
flexDirection: 'row',
},
tableCellContent: { tableCellContent: {
flexGrow: 1, flexGrow: 1,
alignSelf: 'center', alignSelf: 'center',
@ -59,6 +46,8 @@ export default function styles(themeId: number) {
tableCellStatus: { tableCellStatus: {
height: '100%', height: '100%',
alignSelf: 'center', alignSelf: 'center',
border: 'none',
background: 'transparent',
}, },
kbd: { kbd: {
fontFamily: 'sans-serif', fontFamily: 'sans-serif',

View File

@ -0,0 +1,17 @@
.keymap-shortcut-row-content {
display: flex;
flex-direction: row;
> .edit {
opacity: 0;
&:focus-visible, &.-editing {
opacity: 1;
}
}
&:hover > .edit {
opacity: 1;
}
}

View File

@ -0,0 +1,13 @@
.shortcut-recorder {
padding: 2px;
flex-grow: 1;
> .shortcut {
min-height: 29px;
width: 200px;
}
> .shortcut:focus-visible {
border-color: var(--joplin-focus-outline-color);
}
}

View File

@ -9,3 +9,4 @@
@use './editor-toolbar.scss'; @use './editor-toolbar.scss';
@use './user-webview-dialog-container.scss'; @use './user-webview-dialog-container.scss';
@use './dialog-anchor-node.scss'; @use './dialog-anchor-node.scss';
@use './text-input.scss';

View File

@ -0,0 +1,10 @@
.text-input {
height: 24px;
max-height: 24px;
border: 1px solid var(--joplin-divider-color);
padding-left: 5px;
padding-right: 5px;
box-sizing: border-box;
color: var(--joplin-color);
background-color: var(--joplin-background-color);
}

View File

@ -12,6 +12,7 @@
@use 'gui/TrashNotification/style.scss' as trash-notification; @use 'gui/TrashNotification/style.scss' as trash-notification;
@use 'gui/Sidebar/style.scss' as sidebar-styles; @use 'gui/Sidebar/style.scss' as sidebar-styles;
@use 'gui/NoteEditor/style.scss' as note-editor-styles; @use 'gui/NoteEditor/style.scss' as note-editor-styles;
@use 'gui/KeymapConfig/style.scss' as keymap-styles;
@use 'services/plugins/styles/index.scss' as plugins-styles; @use 'services/plugins/styles/index.scss' as plugins-styles;
@use 'gui/styles/index.scss' as gui-styles; @use 'gui/styles/index.scss' as gui-styles;
@use 'main.scss' as main; @use 'main.scss' as main;