1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-11-23 22:36:32 +02:00

Desktop: Accessibility: Improve note title focus handling (#10932)

This commit is contained in:
Henry Heino
2024-08-27 10:05:48 -07:00
committed by GitHub
parent 2afc2ca369
commit 74be949d33
14 changed files with 146 additions and 19 deletions

View File

@@ -27,6 +27,7 @@ import useContextMenu from '../utils/useContextMenu';
import useWebviewIpcMessage from '../utils/useWebviewIpcMessage';
import Toolbar from '../Toolbar';
import useEditorSearchHandler from '../utils/useEditorSearchHandler';
import CommandService from '@joplin/lib/services/CommandService';
const logger = Logger.create('CodeMirror6');
const logDebug = (message: string) => logger.debug(message);
@@ -354,6 +355,10 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
}
}, [editor_scroll, codeMirror_change, props.setLocalSearch, props.setShowLocalSearch]);
const onSelectPastBeginning = useCallback(() => {
void CommandService.instance().execute('focusElement', 'noteTitle');
}, []);
const editorSettings = useMemo((): EditorSettings => {
const isHTMLNote = props.contentMarkupLanguage === MarkupToHtml.MARKUP_LANGUAGE_HTML;
@@ -403,6 +408,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
onEvent={onEditorEvent}
onLogMessage={logDebug}
onEditorPaste={onEditorPaste}
onSelectPastBeginning={onSelectPastBeginning}
externalSearch={props.searchMarkers}
useLocalSearch={props.useLocalSearch}
/>

View File

@@ -9,6 +9,7 @@ import Logger from '@joplin/utils/Logger';
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
import { MarkupLanguage } from '@joplin/renderer';
import { focus } from '@joplin/lib/utils/focusHandler';
import { FocusElementOptions } from '../../../../../commands/focusElement';
const logger = Logger.create('CodeMirror 6 commands');
@@ -103,9 +104,15 @@ const useEditorCommands = (props: Props) => {
logger.warn('CodeMirror execCommand: unsupported command: ', value.name);
}
},
'editor.focus': () => {
'editor.focus': (options?: FocusElementOptions) => {
if (props.visiblePanes.indexOf('editor') >= 0) {
focus('useEditorCommands::editor.focus', editorRef.current.editor);
if (options?.moveCursorToStart) {
editorRef.current.editor.dispatch({
selection: { anchor: 0 },
scrollIntoView: true,
});
}
} else {
// If we just call focus() then the iframe is focused,
// but not its content, such that scrolling up / down

View File

@@ -39,6 +39,7 @@ const { clipboard } = require('electron');
const supportedLocales = require('./supportedLocales');
import { hasProtocol } from '@joplin/utils/url';
import useTabIndenter from './utils/useTabIndenter';
import useKeyboardRefocusHandler from './utils/useKeyboardRefocusHandler';
const logger = Logger.create('TinyMCE');
@@ -130,6 +131,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
usePluginServiceRegistration(ref);
useContextMenu(editor, props.plugins, props.dispatch, props.htmlToMarkdown, props.markupToHtml);
useTabIndenter(editor);
useKeyboardRefocusHandler(editor);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const dispatchDidUpdate = (editor: any) => {
@@ -218,6 +220,13 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
editor.insertContent(result.html);
} else if (cmd.name === 'editor.focus') {
focus('TinyMCE::editor.focus', editor);
if (cmd.value?.moveCursorToStart) {
editor.selection.placeCaretAt(0, 0);
editor.selection.setCursorLocation(
editor.dom.root,
0,
);
}
} else if (cmd.name === 'editor.execCommand') {
if (!('ui' in cmd.value)) cmd.value.ui = false;
if (!('value' in cmd.value)) cmd.value.value = null;

View File

@@ -0,0 +1,43 @@
import CommandService from '@joplin/lib/services/CommandService';
import { useEffect } from 'react';
import type { Editor, EditorEvent } from 'tinymce';
const useKeyboardRefocusHandler = (editor: Editor) => {
useEffect(() => {
if (!editor) return () => {};
const isAtBeginningOf = (element: Node, parent: HTMLElement) => {
if (!parent.contains(element)) return false;
while (
element &&
element.parentNode !== parent &&
element.parentNode?.firstChild === element
) {
element = element.parentNode;
}
return !!element && element.parentNode?.firstChild === element;
};
const eventHandler = (event: EditorEvent<KeyboardEvent>) => {
if (!event.isDefaultPrevented() && event.key === 'ArrowUp') {
const selection = editor.selection.getRng();
if (selection.startOffset === 0 &&
selection.collapsed &&
isAtBeginningOf(selection.startContainer, editor.dom.getRoot())
) {
event.preventDefault();
void CommandService.instance().execute('focusElement', 'noteTitle');
}
}
};
editor.on('keydown', eventHandler);
return () => {
editor.off('keydown', eventHandler);
};
}, [editor]);
};
export default useKeyboardRefocusHandler;

View File

@@ -78,18 +78,13 @@ function styles_(props: Props) {
export default function NoteTitleBar(props: Props) {
const styles = styles_(props);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const onTitleKeydown = useCallback((event: any) => {
const keyCode = event.keyCode;
if (keyCode === 9) { // TAB
const onTitleKeydown: React.KeyboardEventHandler<HTMLInputElement> = useCallback((event) => {
const titleElement = event.currentTarget;
const selectionAtEnd = titleElement.selectionEnd === titleElement.value.length;
if ((event.key === 'ArrowDown' && selectionAtEnd) || event.key === 'Enter') {
event.preventDefault();
if (event.shiftKey) {
void CommandService.instance().execute('focusElement', 'noteList');
} else {
void CommandService.instance().execute('focusElement', 'noteBody');
}
const moveCursorToStart = event.key === 'ArrowDown';
void CommandService.instance().execute('focusElement', 'noteBody', { moveCursorToStart });
}
}, []);

View File

@@ -1,5 +1,6 @@
import { CommandRuntime, CommandDeclaration } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import { FocusElementOptions } from '../../../commands/focusElement';
export const declaration: CommandDeclaration = {
name: 'focusElementNoteBody',
@@ -10,8 +11,8 @@ export const declaration: CommandDeclaration = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
export const runtime = (comp: any): CommandRuntime => {
return {
execute: async () => {
comp.editorRef.current.execCommand({ name: 'editor.focus' });
execute: async (_context: unknown, options?: FocusElementOptions) => {
comp.editorRef.current.execCommand({ name: 'editor.focus', value: options });
},
enabledCondition: 'oneNoteSelected',
};