diff --git a/.eslintignore b/.eslintignore index de61e792e..ec0bdada0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -232,6 +232,9 @@ packages/app-desktop/gui/Dialog.js.map packages/app-desktop/gui/DialogButtonRow.d.ts packages/app-desktop/gui/DialogButtonRow.js packages/app-desktop/gui/DialogButtonRow.js.map +packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.d.ts +packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.js +packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.js.map packages/app-desktop/gui/DialogTitle.d.ts packages/app-desktop/gui/DialogTitle.js packages/app-desktop/gui/DialogTitle.js.map @@ -1060,6 +1063,9 @@ packages/lib/database.js.map packages/lib/debug/DebugService.d.ts packages/lib/debug/DebugService.js packages/lib/debug/DebugService.js.map +packages/lib/dom.d.ts +packages/lib/dom.js +packages/lib/dom.js.map packages/lib/dummy.test.d.ts packages/lib/dummy.test.js packages/lib/dummy.test.js.map diff --git a/.gitignore b/.gitignore index 78c4725a5..fe3bf9e39 100644 --- a/.gitignore +++ b/.gitignore @@ -222,6 +222,9 @@ packages/app-desktop/gui/Dialog.js.map packages/app-desktop/gui/DialogButtonRow.d.ts packages/app-desktop/gui/DialogButtonRow.js packages/app-desktop/gui/DialogButtonRow.js.map +packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.d.ts +packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.js +packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.js.map packages/app-desktop/gui/DialogTitle.d.ts packages/app-desktop/gui/DialogTitle.js packages/app-desktop/gui/DialogTitle.js.map @@ -1050,6 +1053,9 @@ packages/lib/database.js.map packages/lib/debug/DebugService.d.ts packages/lib/debug/DebugService.js packages/lib/debug/DebugService.js.map +packages/lib/dom.d.ts +packages/lib/dom.js +packages/lib/dom.js.map packages/lib/dummy.test.d.ts packages/lib/dummy.test.js packages/lib/dummy.test.js.map diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index 26bfce22e..7da6b46ba 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -554,11 +554,17 @@ class Application extends BaseApplication { // setTimeout(() => { // this.dispatch({ // type: 'DIALOG_OPEN', - // name: 'editFolder', - // props: { folderId: '3d90f7da26b947dc9c8c6c65e86cd231' }, + // name: 'syncWizard', // }); // }, 2000); + // setTimeout(() => { + // this.dispatch({ + // type: 'DIALOG_OPEN', + // name: 'editFolder', + // }); + // }, 3000); + // setTimeout(() => { // this.dispatch({ // type: 'NAV_GO', diff --git a/packages/app-desktop/commands/replaceMisspelling.ts b/packages/app-desktop/commands/replaceMisspelling.ts index b161457a2..a1e0c7e43 100644 --- a/packages/app-desktop/commands/replaceMisspelling.ts +++ b/packages/app-desktop/commands/replaceMisspelling.ts @@ -1,19 +1,12 @@ import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService'; import { AppState } from '../app.reducer'; import bridge from '../services/bridge'; +import { isInsideContainer } from '@joplin/lib/dom'; export const declaration: CommandDeclaration = { name: 'replaceMisspelling', }; -function isInsideContainer(node: any, className: string): boolean { - while (node) { - if (node.classList && node.classList.contains(className)) return true; - node = node.parentNode; - } - return false; -} - export const runtime = (): CommandRuntime => { return { execute: async (context: CommandContext, suggestion: string) => { diff --git a/packages/app-desktop/gui/Dialog.tsx b/packages/app-desktop/gui/Dialog.tsx index 629c32750..42dd3f23d 100644 --- a/packages/app-desktop/gui/Dialog.tsx +++ b/packages/app-desktop/gui/Dialog.tsx @@ -1,4 +1,3 @@ -import { useEffect, useCallback } from 'react'; import styled from 'styled-components'; const DialogModalLayer = styled.div` @@ -33,20 +32,6 @@ interface Props { } export default function Dialog(props: Props) { - const onWindowKeydown = useCallback((event: any) => { - if (event.key === 'Escape') { - if (props.onClose) props.onClose(); - } - }, [props.onClose]); - - useEffect(() => { - window.addEventListener('keydown', onWindowKeydown); - - return () => { - window.removeEventListener('keydown', onWindowKeydown); - }; - }, [onWindowKeydown]); - return ( diff --git a/packages/app-desktop/gui/DialogButtonRow.tsx b/packages/app-desktop/gui/DialogButtonRow.tsx index af40aa987..258cb107a 100644 --- a/packages/app-desktop/gui/DialogButtonRow.tsx +++ b/packages/app-desktop/gui/DialogButtonRow.tsx @@ -1,7 +1,8 @@ -const React = require('react'); -import { useMemo } from 'react'; -const { _ } = require('@joplin/lib/locale'); -const { themeStyle } = require('@joplin/lib/theme'); +import * as React from 'react'; +import { useMemo, useCallback } from 'react'; +import { _ } from '@joplin/lib/locale'; +import { themeStyle } from '@joplin/lib/theme'; +import useKeyboardHandler from './DialogButtonRow/useKeyboardHandler'; export interface ButtonSpec { name: string; @@ -37,32 +38,26 @@ export default function DialogButtonRow(props: Props) { }; }, [theme.buttonStyle]); - const okButton_click = () => { - if (props.onClick) props.onClick({ buttonName: 'ok' }); - }; + const onOkButtonClick = useCallback(() => { + if (props.onClick && !props.okButtonDisabled) props.onClick({ buttonName: 'ok' }); + }, [props.onClick, props.okButtonDisabled]); - const cancelButton_click = () => { - if (props.onClick) props.onClick({ buttonName: 'cancel' }); - }; + const onCancelButtonClick = useCallback(() => { + if (props.onClick && !props.cancelButtonDisabled) props.onClick({ buttonName: 'cancel' }); + }, [props.onClick, props.cancelButtonDisabled]); - const customButton_click = (event: ClickEvent) => { + const onCustomButtonClick = useCallback((event: ClickEvent) => { if (props.onClick) props.onClick(event); - }; + }, [props.onClick]); - const onKeyDown = (event: any) => { - if (event.keyCode === 13) { - okButton_click(); - } else if (event.keyCode === 27) { - cancelButton_click(); - } - }; + const onKeyDown = useKeyboardHandler({ onOkButtonClick, onCancelButtonClick }); const buttonComps = []; if (props.customButtons) { for (const b of props.customButtons) { buttonComps.push( - ); @@ -71,7 +66,7 @@ export default function DialogButtonRow(props: Props) { if (props.okButtonShow !== false) { buttonComps.push( - ); @@ -79,7 +74,7 @@ export default function DialogButtonRow(props: Props) { if (props.cancelButtonShow !== false) { buttonComps.push( - ); diff --git a/packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.ts b/packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.ts new file mode 100644 index 000000000..5527312c7 --- /dev/null +++ b/packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.ts @@ -0,0 +1,62 @@ +import { useEffect, useState, useRef, useCallback } from 'react'; +import { isInsideContainer } from '@joplin/lib/dom'; + +interface Props { + onOkButtonClick: Function; + onCancelButtonClick: Function; +} + +const globalKeydownHandlers: string[] = []; + +export default (props: Props) => { + const [elementId] = useState(`${Math.round(Math.random() * 10000000)}`); + const globalKeydownHandlersRef = useRef(globalKeydownHandlers); + + useEffect(() => { + globalKeydownHandlersRef.current.push(elementId); + return () => { + const idx = globalKeydownHandlersRef.current.findIndex(e => e === elementId); + globalKeydownHandlersRef.current.splice(idx, 1); + }; + }, []); + + const isTopDialog = () => { + const ln = globalKeydownHandlersRef.current.length; + return ln && globalKeydownHandlersRef.current[ln - 1] === elementId; + }; + + const isInSubModal = (targetElement: any) => { + // If we are inside a sub-modal within the dialog, we shouldn't handle + // global key events. It can be for example the emoji picker. In general + // it's difficult to know whether an element is a modal or not, so we'll + // have to add special cases here. Normally there shouldn't be many of + // these. + if (isInsideContainer(targetElement, 'emoji-picker')) return true; + return false; + }; + + const onKeyDown = useCallback((event: any) => { + // Early exit if it's neither ENTER nor ESCAPE, because isInSubModal + // function can be costly. + if (event.keyCode !== 13 && event.keyCode !== 27) return; + + if (!isTopDialog() || isInSubModal(event.target)) return; + + if (event.keyCode === 13) { + if (event.target.nodeName !== 'TEXTAREA') { + props.onOkButtonClick(); + } + } else if (event.keyCode === 27) { + props.onCancelButtonClick(); + } + }, [props.onOkButtonClick, props.onCancelButtonClick]); + + useEffect(() => { + document.addEventListener('keydown', onKeyDown); + return () => { + document.removeEventListener('keydown', onKeyDown); + }; + }, [onKeyDown]); + + return onKeyDown; +}; diff --git a/packages/lib/dom.ts b/packages/lib/dom.ts new file mode 100644 index 000000000..c16a0cd15 --- /dev/null +++ b/packages/lib/dom.ts @@ -0,0 +1,9 @@ +/* eslint-disable import/prefer-default-export */ + +export const isInsideContainer = (node: any, className: string): boolean => { + while (node) { + if (node.classList && node.classList.contains(className)) return true; + node = node.parentNode; + } + return false; +};