7 changes: 4 additions & 3 deletions packages/app-desktop/gui/NotePropertiesDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Note from '@joplin/lib/models/Note';
import bridge from '../services/bridge';
import shim from '@joplin/lib/shim';
import { NoteEntity } from '@joplin/lib/services/database/types';
import { focus } from '@joplin/lib/utils/focusHandler';
const Datetime = require('react-datetime').default;
const { clipboard } = require('electron');
const formatcoords = require('formatcoords');
Expand Down Expand Up @@ -77,7 +78,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {

public componentDidUpdate() {
if (this.state.editedKey === null) {
if (this.okButton.current) this.okButton.current.focus();
if (this.okButton.current) focus('NotePropertiesDialog::componentDidUpdate', this.okButton.current);
}
}

Expand Down Expand Up @@ -220,7 +221,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
if ((this.refs.editField as any).openCalendar) {
(this.refs.editField as any).openCalendar();
} else {
(this.refs.editField as any).focus();
focus('NotePropertiesDialog::editPropertyButtonClick', (this.refs.editField as any));
}
}, 100);
}
Expand Down Expand Up @@ -255,7 +256,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
public async cancelProperty() {
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
return new Promise((resolve: Function) => {
if (this.okButton.current) this.okButton.current.focus();
if (this.okButton.current) focus('NotePropertiesDialog::focus', this.okButton.current);
this.setState({
editedKey: null,
editedValue: null,
Expand Down
5 changes: 4 additions & 1 deletion packages/app-desktop/gui/NoteSearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import { themeStyle } from '@joplin/lib/theme';
import { _ } from '@joplin/lib/locale';
import { focus } from '@joplin/lib/utils/focusHandler';

interface Props {
themeId: number;
Expand Down Expand Up @@ -32,6 +33,8 @@ class NoteSearchBar extends React.Component<Props> {
this.previousButton_click = this.previousButton_click.bind(this);
this.nextButton_click = this.nextButton_click.bind(this);
this.closeButton_click = this.closeButton_click.bind(this);

// eslint-disable-next-line no-restricted-properties
this.focus = this.focus.bind(this);

this.backgroundColor = undefined;
Expand Down Expand Up @@ -125,7 +128,7 @@ class NoteSearchBar extends React.Component<Props> {
}

public focus() {
(this.refs.searchInput as any).focus();
focus('NoteSearchBar::focus', this.refs.searchInput as any);
(this.refs.searchInput as any).select();
}

Expand Down
3 changes: 2 additions & 1 deletion packages/app-desktop/gui/NoteTextViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import PostMessageService, { MessageResponse, ResponderComponentType } from '@joplin/lib/services/PostMessageService';
import * as React from 'react';
import { reg } from '@joplin/lib/registry';
import { focus } from '@joplin/lib/utils/focusHandler';

interface Props {
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
Expand Down Expand Up @@ -119,7 +120,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>

public focus() {
if (this.webviewRef_.current) {
this.webviewRef_.current.focus();
focus('NoteTextViewer::focus', this.webviewRef_.current);
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/app-desktop/gui/PromptDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const Datetime = require('react-datetime').default;
import CreatableSelect from 'react-select/creatable';
import Select from 'react-select';
import makeAnimated from 'react-select/animated';
import { focus } from '@joplin/lib/utils/focusHandler';
interface Props {
themeId: number;
defaultValue: any;
Expand Down Expand Up @@ -67,7 +68,7 @@ export default class PromptDialog extends React.Component<Props, any> {
}

public componentDidUpdate() {
if (this.focusInput_ && this.answerInput_.current) this.answerInput_.current.focus();
if (this.focusInput_ && this.answerInput_.current) focus('PromptDialog::componentDidUpdate', this.answerInput_.current);
this.focusInput_ = false;
}

Expand Down
5 changes: 3 additions & 2 deletions packages/app-desktop/gui/SearchBar/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import uuid from '@joplin/lib/uuid';
const { connect } = require('react-redux');
import Note from '@joplin/lib/models/Note';
import { AppState } from '../../app.reducer';
import { blur, focus } from '@joplin/lib/utils/focusHandler';
const debounce = require('debounce');
const styled = require('styled-components').default;

Expand Down Expand Up @@ -117,7 +118,7 @@ function SearchBar(props: Props) {

const onKeyDown = useCallback((event: any) => {
if (event.key === 'Escape') {
if (document.activeElement) (document.activeElement as any).blur();
if (document.activeElement) blur('SearchBar::onKeyDown', document.activeElement as any);
void onExitSearch();
}
}, [onExitSearch]);
Expand All @@ -127,7 +128,7 @@ function SearchBar(props: Props) {
void onExitSearch();
} else {
setSearchStarted(true);
props.inputRef.current.focus();
focus('SearchBar::onSearchButtonClick', props.inputRef.current);
props.dispatch({
type: 'FOCUS_SET',
field: 'globalSearch',
Expand Down
3 changes: 2 additions & 1 deletion packages/app-desktop/gui/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { RuntimeProps } from './commands/focusElementSideBar';
const { connect } = require('react-redux');
import { renderFolders, renderTags } from '@joplin/lib/components/shared/side-menu-shared';
import { getTrashFolderIcon, getTrashFolderId } from '@joplin/lib/services/trash';
import { focus } from '@joplin/lib/utils/focusHandler';
const { themeStyle } = require('@joplin/lib/theme');
const bridge = require('@electron/remote').require('./bridge').default;
const Menu = bridge().Menu;
Expand Down Expand Up @@ -674,7 +675,7 @@ const SidebarComponent = (props: Props) => {
id: focusItem.id,
});

focusItem.ref.current.focus();
focus('SideBar::onKeyDown', focusItem.ref.current);
}

if (keyCode === 9) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/
import { _ } from '@joplin/lib/locale';
import layoutItemProp from '../../ResizableLayout/utils/layoutItemProp';
import { AppState } from '../../../app.reducer';
import { focus } from '@joplin/lib/utils/focusHandler';

export const declaration: CommandDeclaration = {
name: 'focusElementSideBar',
Expand All @@ -24,10 +25,10 @@ export const runtime = (props: RuntimeProps): CommandRuntime => {
const item = props.getSelectedItem();
if (item) {
const anchorRef = props.anchorItemRefs.current[item.type][item.id];
if (anchorRef) anchorRef.current.focus();
if (anchorRef) focus('focusElementSideBar1', anchorRef.current);
} else {
const anchorRef = props.getFirstAnchorItemRef('folder');
if (anchorRef) anchorRef.current.focus();
if (anchorRef) focus('focusElementSideBar2', anchorRef.current);
}
}
},
Expand Down
3 changes: 2 additions & 1 deletion packages/app-desktop/services/plugins/UserWebview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import useWebviewToPluginMessages from './hooks/useWebviewToPluginMessages';
import useScriptLoader from './hooks/useScriptLoader';
import Logger from '@joplin/utils/Logger';
import styled from 'styled-components';
import { focus } from '@joplin/lib/utils/focusHandler';

const logger = Logger.create('UserWebview');

Expand Down Expand Up @@ -99,7 +100,7 @@ function UserWebview(props: Props, ref: any) {
}
},
focus: function() {
if (viewRef.current) viewRef.current.focus();
if (viewRef.current) focus('UserWebView::focus', viewRef.current);
},
};
});
Expand Down
3 changes: 2 additions & 1 deletion packages/app-desktop/services/plugins/UserWebviewDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import PluginService from '@joplin/lib/services/plugins/PluginService';
import WebviewController from '@joplin/lib/services/plugins/WebviewController';
import UserWebview, { Props as UserWebviewProps } from './UserWebview';
import UserWebviewDialogButtonBar from './UserWebviewDialogButtonBar';
import { focus } from '@joplin/lib/utils/focusHandler';
const styled = require('styled-components').default;

interface Props extends UserWebviewProps {
Expand Down Expand Up @@ -101,7 +102,7 @@ export default function UserWebviewDialog(props: Props) {
// We focus the dialog once it's ready to make sure that the ESC/Enter
// keyboard shortcuts are working.
// https://github.com/laurent22/joplin/issues/4474
if (webviewRef.current) webviewRef.current.focus();
if (webviewRef.current) focus('UserWebviewDialog', webviewRef.current);
}, []);

return (
Expand Down
2 changes: 1 addition & 1 deletion packages/app-desktop/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"extends": "../../tsconfig.json",
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.tsx", "../lib/utils/focusHandler.ts",
],
"exclude": [
"**/node_modules",
Expand Down
3 changes: 2 additions & 1 deletion packages/app-mobile/components/NoteEditor/EditLinkDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { _ } from '@joplin/lib/locale';
import { EditorControl } from './types';
import { useCallback } from 'react';
import SelectionFormatting from '@joplin/editor/SelectionFormatting';
import { focus } from '@joplin/lib/utils/focusHandler';

interface LinkDialogProps {
editorControl: EditorControl;
Expand Down Expand Up @@ -100,7 +101,7 @@ const EditLinkDialog = (props: LinkDialogProps) => {
autoFocus

onSubmitEditing={() => {
linkInputRef.current.focus();
focus('EditLinkDialog::onSubmitEditing', linkInputRef.current);
}}
onChangeText={(text: string) => setLinkLabel(text)}
/>
Expand Down
8 changes: 2 additions & 6 deletions packages/app-mobile/components/screens/Note.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import { getDisplayParentTitle } from '@joplin/lib/services/trash';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
import pickDocument from '../../utils/pickDocument';
import debounce from '../../utils/debounce';
import { focus } from '@joplin/lib/utils/focusHandler';
const urlUtils = require('@joplin/lib/urlUtils');

const emptyArray: any[] = [];
Expand Down Expand Up @@ -1328,13 +1329,8 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
// Avoid writing `this.titleTextFieldRef.current` -- titleTextFieldRef may
// be undefined.
if (fieldToFocus === 'title' && this.titleTextFieldRef?.current) {
this.titleTextFieldRef.current.focus();
focus('Note::focusUpdate', this.titleTextFieldRef.current);
}
// if (fieldToFocus === 'body' && this.markdownEditorRef.current) {
// if (this.markdownEditorRef.current) {
// this.markdownEditorRef.current.focus();
// }
// }
}

private async folderPickerOptions_valueChanged(itemValue: any) {
Expand Down
3 changes: 2 additions & 1 deletion packages/editor/CodeMirror/editorCommands/editorCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import swapLine, { SwapLineDirection } from './swapLine';
import duplicateLine from './duplicateLine';
import sortSelectedLines from './sortSelectedLines';
import { closeSearchPanel, findNext, findPrevious, openSearchPanel, replaceAll, replaceNext } from '@codemirror/search';
import { focus } from '@joplin/lib/utils/focusHandler';

export type EditorCommandFunction = (editor: EditorView, ...args: any[])=> void|any;

Expand All @@ -22,7 +23,7 @@ const editorCommands: Record<EditorCommandType, EditorCommandFunction> = {
[EditorCommandType.Undo]: undo,
[EditorCommandType.Redo]: redo,
[EditorCommandType.SelectAll]: selectAll,
[EditorCommandType.Focus]: editor => editor.focus(),
[EditorCommandType.Focus]: editor => focus('editorCommands::focus', editor),

[EditorCommandType.ToggleBolded]: toggleBolded,
[EditorCommandType.ToggleItalicized]: toggleItalicized,
Expand Down
41 changes: 41 additions & 0 deletions packages/lib/utils/focusHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// The purpose of this handler is to have all focus/blur calls go through the same place, which
// makes it easier to log what happens. This is useful when one unknown component is stealing focus
// from another component. Potentially it could also be used to resolve conflict situations when
// multiple components try to set the focus at the same time.

import Logger from '@joplin/utils/Logger';

const logger = Logger.create('setFocus');

enum ToggleFocusAction {
Focus = 'focus',
Blur = 'blur',
}

interface FocusableElement {
focus: ()=> void;
blur: ()=> void;
}

const toggleFocus = (source: string, element: FocusableElement, action: ToggleFocusAction) => {
if (!element) {
logger.warn(`Tried action "${action}" on an undefined element: ${source}`);
return;
}

if (!element[action]) {
logger.warn(`Element does not have a "${action}" method: ${source}`);
return;
}

logger.debug(`Action "${action}" from "${source}"`);
element[action]();
};

export const focus = (source: string, element: any) => {
toggleFocus(source, element, ToggleFocusAction.Focus);
};

export const blur = (source: string, element: any) => {
toggleFocus(source, element, ToggleFocusAction.Blur);
};
3 changes: 2 additions & 1 deletion packages/pdf-viewer/PdfDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ export default class PdfDocument {
frame.contentWindow.onafterprint = () => {
frame.remove();
};
frame.focus();
console.warn('frame.focus() has been disabled!! Use focusHandler instead');
// frame.focus();
frame.contentWindow.print();
};
frame.src = this.url as string;
Expand Down