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.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; | ||||
| }; | ||||
		Reference in New Issue
	
	Block a user