You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Desktop: Resovles #6194: Improved handling of ENTER and ESCAPE keys in dialogs
This commit is contained in:
		| @@ -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; | ||||||
|  | }; | ||||||
		Reference in New Issue
	
	Block a user