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.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
|
||||
|
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.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
|
||||
|
@ -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',
|
||||
|
@ -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) => {
|
||||
|
@ -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 (
|
||||
<DialogModalLayer className={props.className}>
|
||||
<DialogRoot>
|
||||
|
@ -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(
|
||||
<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}
|
||||
</button>
|
||||
);
|
||||
@ -71,7 +66,7 @@ export default function DialogButtonRow(props: Props) {
|
||||
|
||||
if (props.okButtonShow !== false) {
|
||||
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')}
|
||||
</button>
|
||||
);
|
||||
@ -79,7 +74,7 @@ export default function DialogButtonRow(props: Props) {
|
||||
|
||||
if (props.cancelButtonShow !== false) {
|
||||
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')}
|
||||
</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