mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-02 12:47:41 +02:00
Desktop: Resovles #6194: Improved handling of ENTER and ESCAPE keys in dialogs
This commit is contained in:
parent
ff066baa26
commit
558e55090f
@ -232,6 +232,9 @@ packages/app-desktop/gui/Dialog.js.map
|
|||||||
packages/app-desktop/gui/DialogButtonRow.d.ts
|
packages/app-desktop/gui/DialogButtonRow.d.ts
|
||||||
packages/app-desktop/gui/DialogButtonRow.js
|
packages/app-desktop/gui/DialogButtonRow.js
|
||||||
packages/app-desktop/gui/DialogButtonRow.js.map
|
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.d.ts
|
||||||
packages/app-desktop/gui/DialogTitle.js
|
packages/app-desktop/gui/DialogTitle.js
|
||||||
packages/app-desktop/gui/DialogTitle.js.map
|
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.d.ts
|
||||||
packages/lib/debug/DebugService.js
|
packages/lib/debug/DebugService.js
|
||||||
packages/lib/debug/DebugService.js.map
|
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.d.ts
|
||||||
packages/lib/dummy.test.js
|
packages/lib/dummy.test.js
|
||||||
packages/lib/dummy.test.js.map
|
packages/lib/dummy.test.js.map
|
||||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -222,6 +222,9 @@ packages/app-desktop/gui/Dialog.js.map
|
|||||||
packages/app-desktop/gui/DialogButtonRow.d.ts
|
packages/app-desktop/gui/DialogButtonRow.d.ts
|
||||||
packages/app-desktop/gui/DialogButtonRow.js
|
packages/app-desktop/gui/DialogButtonRow.js
|
||||||
packages/app-desktop/gui/DialogButtonRow.js.map
|
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.d.ts
|
||||||
packages/app-desktop/gui/DialogTitle.js
|
packages/app-desktop/gui/DialogTitle.js
|
||||||
packages/app-desktop/gui/DialogTitle.js.map
|
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.d.ts
|
||||||
packages/lib/debug/DebugService.js
|
packages/lib/debug/DebugService.js
|
||||||
packages/lib/debug/DebugService.js.map
|
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.d.ts
|
||||||
packages/lib/dummy.test.js
|
packages/lib/dummy.test.js
|
||||||
packages/lib/dummy.test.js.map
|
packages/lib/dummy.test.js.map
|
||||||
|
@ -554,11 +554,17 @@ class Application extends BaseApplication {
|
|||||||
// setTimeout(() => {
|
// setTimeout(() => {
|
||||||
// this.dispatch({
|
// this.dispatch({
|
||||||
// type: 'DIALOG_OPEN',
|
// type: 'DIALOG_OPEN',
|
||||||
// name: 'editFolder',
|
// name: 'syncWizard',
|
||||||
// props: { folderId: '3d90f7da26b947dc9c8c6c65e86cd231' },
|
|
||||||
// });
|
// });
|
||||||
// }, 2000);
|
// }, 2000);
|
||||||
|
|
||||||
|
// setTimeout(() => {
|
||||||
|
// this.dispatch({
|
||||||
|
// type: 'DIALOG_OPEN',
|
||||||
|
// name: 'editFolder',
|
||||||
|
// });
|
||||||
|
// }, 3000);
|
||||||
|
|
||||||
// setTimeout(() => {
|
// setTimeout(() => {
|
||||||
// this.dispatch({
|
// this.dispatch({
|
||||||
// type: 'NAV_GO',
|
// type: 'NAV_GO',
|
||||||
|
@ -1,19 +1,12 @@
|
|||||||
import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||||
import { AppState } from '../app.reducer';
|
import { AppState } from '../app.reducer';
|
||||||
import bridge from '../services/bridge';
|
import bridge from '../services/bridge';
|
||||||
|
import { isInsideContainer } from '@joplin/lib/dom';
|
||||||
|
|
||||||
export const declaration: CommandDeclaration = {
|
export const declaration: CommandDeclaration = {
|
||||||
name: 'replaceMisspelling',
|
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 => {
|
export const runtime = (): CommandRuntime => {
|
||||||
return {
|
return {
|
||||||
execute: async (context: CommandContext, suggestion: string) => {
|
execute: async (context: CommandContext, suggestion: string) => {
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { useEffect, useCallback } from 'react';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const DialogModalLayer = styled.div`
|
const DialogModalLayer = styled.div`
|
||||||
@ -33,20 +32,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Dialog(props: 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 (
|
return (
|
||||||
<DialogModalLayer className={props.className}>
|
<DialogModalLayer className={props.className}>
|
||||||
<DialogRoot>
|
<DialogRoot>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
const React = require('react');
|
import * as React from 'react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo, useCallback } from 'react';
|
||||||
const { _ } = require('@joplin/lib/locale');
|
import { _ } from '@joplin/lib/locale';
|
||||||
const { themeStyle } = require('@joplin/lib/theme');
|
import { themeStyle } from '@joplin/lib/theme';
|
||||||
|
import useKeyboardHandler from './DialogButtonRow/useKeyboardHandler';
|
||||||
|
|
||||||
export interface ButtonSpec {
|
export interface ButtonSpec {
|
||||||
name: string;
|
name: string;
|
||||||
@ -37,32 +38,26 @@ export default function DialogButtonRow(props: Props) {
|
|||||||
};
|
};
|
||||||
}, [theme.buttonStyle]);
|
}, [theme.buttonStyle]);
|
||||||
|
|
||||||
const okButton_click = () => {
|
const onOkButtonClick = useCallback(() => {
|
||||||
if (props.onClick) props.onClick({ buttonName: 'ok' });
|
if (props.onClick && !props.okButtonDisabled) props.onClick({ buttonName: 'ok' });
|
||||||
};
|
}, [props.onClick, props.okButtonDisabled]);
|
||||||
|
|
||||||
const cancelButton_click = () => {
|
const onCancelButtonClick = useCallback(() => {
|
||||||
if (props.onClick) props.onClick({ buttonName: 'cancel' });
|
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);
|
if (props.onClick) props.onClick(event);
|
||||||
};
|
}, [props.onClick]);
|
||||||
|
|
||||||
const onKeyDown = (event: any) => {
|
const onKeyDown = useKeyboardHandler({ onOkButtonClick, onCancelButtonClick });
|
||||||
if (event.keyCode === 13) {
|
|
||||||
okButton_click();
|
|
||||||
} else if (event.keyCode === 27) {
|
|
||||||
cancelButton_click();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const buttonComps = [];
|
const buttonComps = [];
|
||||||
|
|
||||||
if (props.customButtons) {
|
if (props.customButtons) {
|
||||||
for (const b of props.customButtons) {
|
for (const b of props.customButtons) {
|
||||||
buttonComps.push(
|
buttonComps.push(
|
||||||
<button key={b.name} style={buttonStyle} onClick={() => customButton_click({ buttonName: b.name })} onKeyDown={onKeyDown}>
|
<button key={b.name} style={buttonStyle} onClick={() => onCustomButtonClick({ buttonName: b.name })} onKeyDown={onKeyDown}>
|
||||||
{b.label}
|
{b.label}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@ -71,7 +66,7 @@ export default function DialogButtonRow(props: Props) {
|
|||||||
|
|
||||||
if (props.okButtonShow !== false) {
|
if (props.okButtonShow !== false) {
|
||||||
buttonComps.push(
|
buttonComps.push(
|
||||||
<button disabled={props.okButtonDisabled} key="ok" style={buttonStyle} onClick={okButton_click} ref={props.okButtonRef} onKeyDown={onKeyDown}>
|
<button disabled={props.okButtonDisabled} key="ok" style={buttonStyle} onClick={onOkButtonClick} ref={props.okButtonRef} onKeyDown={onKeyDown}>
|
||||||
{props.okButtonLabel ? props.okButtonLabel : _('OK')}
|
{props.okButtonLabel ? props.okButtonLabel : _('OK')}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@ -79,7 +74,7 @@ export default function DialogButtonRow(props: Props) {
|
|||||||
|
|
||||||
if (props.cancelButtonShow !== false) {
|
if (props.cancelButtonShow !== false) {
|
||||||
buttonComps.push(
|
buttonComps.push(
|
||||||
<button disabled={props.cancelButtonDisabled} key="cancel" style={Object.assign({}, buttonStyle)} onClick={cancelButton_click}>
|
<button disabled={props.cancelButtonDisabled} key="cancel" style={Object.assign({}, buttonStyle)} onClick={onCancelButtonClick}>
|
||||||
{props.cancelButtonLabel ? props.cancelButtonLabel : _('Cancel')}
|
{props.cancelButtonLabel ? props.cancelButtonLabel : _('Cancel')}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
|
};
|
9
packages/lib/dom.ts
Normal file
9
packages/lib/dom.ts
Normal file
@ -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;
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user