diff --git a/src/actions/actionFinalize.tsx b/src/actions/actionFinalize.tsx index 30db774f..d314f1a3 100644 --- a/src/actions/actionFinalize.tsx +++ b/src/actions/actionFinalize.tsx @@ -18,7 +18,7 @@ import { isBindingElement } from "../element/typeChecks"; export const actionFinalize = register({ name: "finalize", - perform: (elements, appState, _, { canvas }) => { + perform: (elements, appState, _, { canvas, focusContainer }) => { if (appState.editingLinearElement) { const { elementId, @@ -51,7 +51,7 @@ export const actionFinalize = register({ let newElements = elements; if (window.document.activeElement instanceof HTMLElement) { - window.document.activeElement.blur(); + focusContainer(); } const multiPointElement = appState.multiElement diff --git a/src/actions/actionMenu.tsx b/src/actions/actionMenu.tsx index 8cfae398..18d56dc4 100644 --- a/src/actions/actionMenu.tsx +++ b/src/actions/actionMenu.tsx @@ -70,7 +70,10 @@ export const actionFullScreen = register({ export const actionShortcuts = register({ name: "toggleShortcuts", - perform: (_elements, appState) => { + perform: (_elements, appState, _, { focusContainer }) => { + if (appState.showHelpDialog) { + focusContainer(); + } return { appState: { ...appState, diff --git a/src/actions/manager.tsx b/src/actions/manager.tsx index 3cdc81f0..a0339ffa 100644 --- a/src/actions/manager.tsx +++ b/src/actions/manager.tsx @@ -12,7 +12,11 @@ import { MODES } from "../constants"; // This is the component, but for now we don't care about anything but its // `canvas` state. -type App = { canvas: HTMLCanvasElement | null; props: AppProps }; +type App = { + canvas: HTMLCanvasElement | null; + focusContainer: () => void; + props: AppProps; +}; export class ActionManager implements ActionsManagerInterface { actions = {} as ActionsManagerInterface["actions"]; @@ -51,7 +55,7 @@ export class ActionManager implements ActionsManagerInterface { actions.forEach((action) => this.registerAction(action)); } - handleKeyDown(event: KeyboardEvent) { + handleKeyDown(event: React.KeyboardEvent | KeyboardEvent) { const canvasActions = this.app.props.UIOptions.canvasActions; const data = Object.values(this.actions) .sort((a, b) => (b.keyPriority || 0) - (a.keyPriority || 0)) diff --git a/src/actions/types.ts b/src/actions/types.ts index 1597714d..5af6faa3 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -15,11 +15,13 @@ export type ActionResult = } | false; +type AppAPI = { canvas: HTMLCanvasElement | null; focusContainer(): void }; + type ActionFn = ( elements: readonly ExcalidrawElement[], appState: Readonly, formData: any, - app: { canvas: HTMLCanvasElement | null }, + app: AppAPI, ) => ActionResult | Promise; export type UpdaterFn = (res: ActionResult) => void; @@ -105,7 +107,7 @@ export interface Action { perform: ActionFn; keyPriority?: number; keyTest?: ( - event: KeyboardEvent, + event: React.KeyboardEvent | KeyboardEvent, appState: AppState, elements: readonly ExcalidrawElement[], ) => boolean; @@ -120,6 +122,6 @@ export interface Action { export interface ActionsManagerInterface { actions: Record; registerAction: (action: Action) => void; - handleKeyDown: (event: KeyboardEvent) => boolean; + handleKeyDown: (event: React.KeyboardEvent | KeyboardEvent) => boolean; renderAction: (name: ActionName) => React.ReactElement | null; } diff --git a/src/components/App.tsx b/src/components/App.tsx index 4b4b6c92..4961fa19 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -445,12 +445,16 @@ class App extends React.Component { return (
{ } libraryReturnUrl={this.props.libraryReturnUrl} UIOptions={this.props.UIOptions} + focusContainer={this.focusContainer} />
@@ -509,6 +514,10 @@ class App extends React.Component { ); } + public focusContainer = () => { + this.excalidrawContainerRef.current?.focus(); + }; + public getSceneElementsIncludingDeleted = () => { return this.scene.getElementsIncludingDeleted(); }; @@ -655,6 +664,8 @@ class App extends React.Component { } catch (error) { window.alert(t("alerts.errorLoadingLibrary")); console.error(error); + } finally { + this.focusContainer(); } }; @@ -795,6 +806,10 @@ class App extends React.Component { this.scene.addCallback(this.onSceneUpdated); this.addEventListeners(); + if (this.excalidrawContainerRef.current) { + this.focusContainer(); + } + if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) { this.resizeObserver = new ResizeObserver(() => { // compute isMobile state @@ -854,7 +869,6 @@ class App extends React.Component { EVENT.SCROLL, this.onScroll, ); - document.removeEventListener(EVENT.KEYDOWN, this.onKeyDown, false); document.removeEventListener( EVENT.MOUSE_MOVE, @@ -890,7 +904,9 @@ class App extends React.Component { private addEventListeners() { this.removeEventListeners(); document.addEventListener(EVENT.COPY, this.onCopy); - document.addEventListener(EVENT.KEYDOWN, this.onKeyDown, false); + if (this.props.handleKeyboardGlobally) { + document.addEventListener(EVENT.KEYDOWN, this.onKeyDown, false); + } document.addEventListener(EVENT.KEYUP, this.onKeyUp, { passive: true }); document.addEventListener( EVENT.MOUSE_MOVE, @@ -1434,152 +1450,156 @@ class App extends React.Component { // Input handling - private onKeyDown = withBatchedUpdates((event: KeyboardEvent) => { - // normalize `event.key` when CapsLock is pressed #2372 - if ( - "Proxy" in window && - ((!event.shiftKey && /^[A-Z]$/.test(event.key)) || - (event.shiftKey && /^[a-z]$/.test(event.key))) - ) { - event = new Proxy(event, { - get(ev: any, prop) { - const value = ev[prop]; - if (typeof value === "function") { - // fix for Proxies hijacking `this` - return value.bind(ev); - } - return prop === "key" - ? // CapsLock inverts capitalization based on ShiftKey, so invert - // it back - event.shiftKey - ? ev.key.toUpperCase() - : ev.key.toLowerCase() - : value; - }, - }); - } - - if ( - (isWritableElement(event.target) && event.key !== KEYS.ESCAPE) || - // case: using arrows to move between buttons - (isArrowKey(event.key) && isInputLike(event.target)) - ) { - return; - } - - if (event.key === KEYS.QUESTION_MARK) { - this.setState({ - showHelpDialog: true, - }); - } - - if (this.actionManager.handleKeyDown(event)) { - return; - } - - if (this.state.viewModeEnabled) { - return; - } - - if (event[KEYS.CTRL_OR_CMD] && this.state.isBindingEnabled) { - this.setState({ isBindingEnabled: false }); - } - - if (event.code === CODES.NINE) { - this.setState({ isLibraryOpen: !this.state.isLibraryOpen }); - } - - if (isArrowKey(event.key)) { - const step = - (this.state.gridSize && - (event.shiftKey ? ELEMENT_TRANSLATE_AMOUNT : this.state.gridSize)) || - (event.shiftKey - ? ELEMENT_SHIFT_TRANSLATE_AMOUNT - : ELEMENT_TRANSLATE_AMOUNT); - - const selectedElements = this.scene - .getElements() - .filter((element) => this.state.selectedElementIds[element.id]); - - let offsetX = 0; - let offsetY = 0; - - if (event.key === KEYS.ARROW_LEFT) { - offsetX = -step; - } else if (event.key === KEYS.ARROW_RIGHT) { - offsetX = step; - } else if (event.key === KEYS.ARROW_UP) { - offsetY = -step; - } else if (event.key === KEYS.ARROW_DOWN) { - offsetY = step; + private onKeyDown = withBatchedUpdates( + (event: React.KeyboardEvent | KeyboardEvent) => { + // normalize `event.key` when CapsLock is pressed #2372 + if ( + "Proxy" in window && + ((!event.shiftKey && /^[A-Z]$/.test(event.key)) || + (event.shiftKey && /^[a-z]$/.test(event.key))) + ) { + event = new Proxy(event, { + get(ev: any, prop) { + const value = ev[prop]; + if (typeof value === "function") { + // fix for Proxies hijacking `this` + return value.bind(ev); + } + return prop === "key" + ? // CapsLock inverts capitalization based on ShiftKey, so invert + // it back + event.shiftKey + ? ev.key.toUpperCase() + : ev.key.toLowerCase() + : value; + }, + }); } - selectedElements.forEach((element) => { - mutateElement(element, { - x: element.x + offsetX, - y: element.y + offsetY, - }); - - updateBoundElements(element, { - simultaneouslyUpdated: selectedElements, - }); - }); - - this.maybeSuggestBindingForAll(selectedElements); - - event.preventDefault(); - } else if (event.key === KEYS.ENTER) { - const selectedElements = getSelectedElements( - this.scene.getElements(), - this.state, - ); - if ( - selectedElements.length === 1 && - isLinearElement(selectedElements[0]) + (isWritableElement(event.target) && event.key !== KEYS.ESCAPE) || + // case: using arrows to move between buttons + (isArrowKey(event.key) && isInputLike(event.target)) ) { - if ( - !this.state.editingLinearElement || - this.state.editingLinearElement.elementId !== selectedElements[0].id - ) { - history.resumeRecording(); - this.setState({ - editingLinearElement: new LinearElementEditor( - selectedElements[0], - this.scene, - ), - }); - } - } else if ( - selectedElements.length === 1 && - !isLinearElement(selectedElements[0]) - ) { - const selectedElement = selectedElements[0]; - this.startTextEditing({ - sceneX: selectedElement.x + selectedElement.width / 2, - sceneY: selectedElement.y + selectedElement.height / 2, - }); - event.preventDefault(); return; } - } else if ( - !event.ctrlKey && - !event.altKey && - !event.metaKey && - this.state.draggingElement === null - ) { - const shape = findShapeByKey(event.key); - if (shape) { - this.selectShapeTool(shape); - } else if (event.key === KEYS.Q) { - this.toggleLock(); + + if (event.key === KEYS.QUESTION_MARK) { + this.setState({ + showHelpDialog: true, + }); } - } - if (event.key === KEYS.SPACE && gesture.pointers.size === 0) { - isHoldingSpace = true; - setCursor(this.canvas, CURSOR_TYPE.GRABBING); - } - }); + + if (this.actionManager.handleKeyDown(event)) { + return; + } + + if (this.state.viewModeEnabled) { + return; + } + + if (event[KEYS.CTRL_OR_CMD] && this.state.isBindingEnabled) { + this.setState({ isBindingEnabled: false }); + } + + if (event.code === CODES.NINE) { + this.setState({ isLibraryOpen: !this.state.isLibraryOpen }); + } + + if (isArrowKey(event.key)) { + const step = + (this.state.gridSize && + (event.shiftKey + ? ELEMENT_TRANSLATE_AMOUNT + : this.state.gridSize)) || + (event.shiftKey + ? ELEMENT_SHIFT_TRANSLATE_AMOUNT + : ELEMENT_TRANSLATE_AMOUNT); + + const selectedElements = this.scene + .getElements() + .filter((element) => this.state.selectedElementIds[element.id]); + + let offsetX = 0; + let offsetY = 0; + + if (event.key === KEYS.ARROW_LEFT) { + offsetX = -step; + } else if (event.key === KEYS.ARROW_RIGHT) { + offsetX = step; + } else if (event.key === KEYS.ARROW_UP) { + offsetY = -step; + } else if (event.key === KEYS.ARROW_DOWN) { + offsetY = step; + } + + selectedElements.forEach((element) => { + mutateElement(element, { + x: element.x + offsetX, + y: element.y + offsetY, + }); + + updateBoundElements(element, { + simultaneouslyUpdated: selectedElements, + }); + }); + + this.maybeSuggestBindingForAll(selectedElements); + + event.preventDefault(); + } else if (event.key === KEYS.ENTER) { + const selectedElements = getSelectedElements( + this.scene.getElements(), + this.state, + ); + + if ( + selectedElements.length === 1 && + isLinearElement(selectedElements[0]) + ) { + if ( + !this.state.editingLinearElement || + this.state.editingLinearElement.elementId !== selectedElements[0].id + ) { + history.resumeRecording(); + this.setState({ + editingLinearElement: new LinearElementEditor( + selectedElements[0], + this.scene, + ), + }); + } + } else if ( + selectedElements.length === 1 && + !isLinearElement(selectedElements[0]) + ) { + const selectedElement = selectedElements[0]; + this.startTextEditing({ + sceneX: selectedElement.x + selectedElement.width / 2, + sceneY: selectedElement.y + selectedElement.height / 2, + }); + event.preventDefault(); + return; + } + } else if ( + !event.ctrlKey && + !event.altKey && + !event.metaKey && + this.state.draggingElement === null + ) { + const shape = findShapeByKey(event.key); + if (shape) { + this.selectShapeTool(shape); + } else if (event.key === KEYS.Q) { + this.toggleLock(); + } + } + if (event.key === KEYS.SPACE && gesture.pointers.size === 0) { + isHoldingSpace = true; + setCursor(this.canvas, CURSOR_TYPE.GRABBING); + } + }, + ); private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => { if (event.key === KEYS.SPACE) { @@ -1615,7 +1635,7 @@ class App extends React.Component { setCursorForShape(this.canvas, elementType); } if (isToolIcon(document.activeElement)) { - document.activeElement.blur(); + this.focusContainer(); } if (!isLinearElementType(elementType)) { this.setState({ suggestedBindings: [] }); @@ -1745,6 +1765,8 @@ class App extends React.Component { if (this.state.elementLocked) { setCursorForShape(this.canvas, this.state.elementType); } + + this.focusContainer(); }), element, }); diff --git a/src/components/ColorPicker.tsx b/src/components/ColorPicker.tsx index 82d6d9e1..bc353e58 100644 --- a/src/components/ColorPicker.tsx +++ b/src/components/ColorPicker.tsx @@ -115,6 +115,7 @@ const Picker = ({ onClose(); } event.nativeEvent.stopImmediatePropagation(); + event.stopPropagation(); }; return ( diff --git a/src/components/Dialog.tsx b/src/components/Dialog.tsx index 26467517..c1d71fef 100644 --- a/src/components/Dialog.tsx +++ b/src/components/Dialog.tsx @@ -18,7 +18,6 @@ export const Dialog = (props: { autofocus?: boolean; }) => { const [islandNode, setIslandNode] = useCallbackRefState(); - useEffect(() => { if (!islandNode) { return; diff --git a/src/components/ErrorDialog.tsx b/src/components/ErrorDialog.tsx index 2886222c..63493f80 100644 --- a/src/components/ErrorDialog.tsx +++ b/src/components/ErrorDialog.tsx @@ -18,6 +18,7 @@ export const ErrorDialog = ({ if (onClose) { onClose(); } + document.querySelector(".excalidraw-container")?.focus(); }, [onClose]); return ( diff --git a/src/components/IconPicker.tsx b/src/components/IconPicker.tsx index 77e9d5f8..9b6d7ea5 100644 --- a/src/components/IconPicker.tsx +++ b/src/components/IconPicker.tsx @@ -88,6 +88,7 @@ function Picker({ onClose(); } event.nativeEvent.stopImmediatePropagation(); + event.stopPropagation(); }; return ( diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index 304bb048..67b579d4 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -72,6 +72,7 @@ interface LayerUIProps { viewModeEnabled: boolean; libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; UIOptions: AppProps["UIOptions"]; + focusContainer: () => void; } const useOnClickOutside = ( @@ -111,6 +112,7 @@ const LibraryMenuItems = ({ setAppState, setLibraryItems, libraryReturnUrl, + focusContainer, }: { library: LibraryItems; pendingElements: LibraryItem; @@ -120,6 +122,7 @@ const LibraryMenuItems = ({ setAppState: React.Component["setState"]; setLibraryItems: (library: LibraryItems) => void; libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; + focusContainer: () => void; }) => { const isMobile = useIsMobile(); const numCells = library.length + (pendingElements.length > 0 ? 1 : 0); @@ -178,6 +181,7 @@ const LibraryMenuItems = ({ if (window.confirm(t("alerts.resetLibrary"))) { Library.resetLibrary(); setLibraryItems([]); + focusContainer(); } }} /> @@ -242,6 +246,7 @@ const LibraryMenu = ({ onAddToLibrary, setAppState, libraryReturnUrl, + focusContainer, }: { pendingElements: LibraryItem; onClickOutside: (event: MouseEvent) => void; @@ -249,6 +254,7 @@ const LibraryMenu = ({ onAddToLibrary: () => void; setAppState: React.Component["setState"]; libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; + focusContainer: () => void; }) => { const ref = useRef(null); useOnClickOutside(ref, (event) => { @@ -322,6 +328,7 @@ const LibraryMenu = ({ setAppState={setAppState} setLibraryItems={setLibraryItems} libraryReturnUrl={libraryReturnUrl} + focusContainer={focusContainer} /> )} @@ -347,6 +354,7 @@ const LayerUI = ({ viewModeEnabled, libraryReturnUrl, UIOptions, + focusContainer, }: LayerUIProps) => { const isMobile = useIsMobile(); @@ -517,6 +525,7 @@ const LayerUI = ({ onAddToLibrary={deselectItems} setAppState={setAppState} libraryReturnUrl={libraryReturnUrl} + focusContainer={focusContainer} /> ) : null; @@ -660,7 +669,15 @@ const LayerUI = ({ /> )} {appState.showHelpDialog && ( - setAppState({ showHelpDialog: false })} /> + { + const helpIcon = document.querySelector( + ".help-icon", + )! as HTMLElement; + helpIcon.focus(); + setAppState({ showHelpDialog: false }); + }} + /> )} {appState.pasteDialog.shown && ( { if (event.key === KEYS.ESCAPE) { event.nativeEvent.stopImmediatePropagation(); + event.stopPropagation(); props.onCloseRequest(); } }; @@ -38,6 +39,7 @@ export const Modal = (props: {
{props.children}
diff --git a/src/components/ProjectName.tsx b/src/components/ProjectName.tsx index a7d723ef..9e7af01e 100644 --- a/src/components/ProjectName.tsx +++ b/src/components/ProjectName.tsx @@ -1,6 +1,7 @@ import "./TextInput.scss"; import React, { Component } from "react"; +import { focusNearestParent } from "../utils"; type Props = { value: string; @@ -17,6 +18,7 @@ export class ProjectName extends Component { fileName: this.props.value, }; private handleBlur = (event: any) => { + focusNearestParent(event.target); const value = event.target.value; if (value !== this.props.value) { this.props.onChange(value); diff --git a/src/css/styles.scss b/src/css/styles.scss index 164c9423..aac01100 100644 --- a/src/css/styles.scss +++ b/src/css/styles.scss @@ -19,6 +19,10 @@ height: 100%; width: 100%; + &:focus { + outline: none; + } + // serves 2 purposes: // 1. prevent selecting text outside the component when double-clicking or // dragging inside it (e.g. on canvas) diff --git a/src/element/textWysiwyg.tsx b/src/element/textWysiwyg.tsx index 862a9577..1329e13b 100644 --- a/src/element/textWysiwyg.tsx +++ b/src/element/textWysiwyg.tsx @@ -159,11 +159,14 @@ export const textWysiwyg = ({ // so that we don't need to create separate a callback for event handlers let submittedViaKeyboard = false; const handleSubmit = () => { + // cleanup must be run before onSubmit otherwise when app blurs the wysiwyg + // it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the + // wysiwyg on update + cleanup(); onSubmit({ text: normalizeText(editable.value), viaKeyboard: submittedViaKeyboard, }); - cleanup(); }; const cleanup = () => { diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index 3a9763af..36915d6c 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -324,6 +324,7 @@ const ExcalidrawWrapper = () => { langCode={langCode} renderCustomStats={renderCustomStats} detectScroll={false} + handleKeyboardGlobally={true} /> {excalidrawAPI && } {errorMessage && ( diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index 71a2b354..5fbc2b3e 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -15,6 +15,14 @@ Please add the latest change on the top under the correct section. ## Excalidraw API +### Features + +- Bind the keyboard events to component and added a prop [`handleKeyboardGlobally`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#handleKeyboardGlobally) which if set to true will bind the keyboard events to document [#3430](https://github.com/excalidraw/excalidraw/pull/3430). + + #### BREAKING CHNAGE + + - Earlier keyboard events were bind to document but now its bind to Excalidraw component by default. So you will need to set [`handleKeyboardGlobally`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#handleKeyboardGlobally) to true if you want the previous behaviour (bind the keyboard events to document). + - Recompute offsets on `scroll` of the nearest scrollable container [#3408](https://github.com/excalidraw/excalidraw/pull/3408). This can be disabled by setting [`detectScroll`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#detectScroll) to `false`. - Add `onPaste` prop to handle custom clipboard behaviours [#3420](https://github.com/excalidraw/excalidraw/pull/3420). diff --git a/src/packages/excalidraw/README_NEXT.md b/src/packages/excalidraw/README_NEXT.md index 49c86126..488985ea 100644 --- a/src/packages/excalidraw/README_NEXT.md +++ b/src/packages/excalidraw/README_NEXT.md @@ -366,6 +366,7 @@ To view the full example visit :point_down: | [`UIOptions`](#UIOptions) |
{ canvasActions:  CanvasActions }
| [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L129) | To customise UI options. Currently we support customising [`canvas actions`](#canvasActions) | | [`onPaste`](#onPaste) |
(data: ClipboardData, event: ClipboardEvent | null) => boolean
| | Callback to be triggered if passed when the something is pasted in to the scene | | [`detectScroll`](#detectScroll) | boolean | true | Indicates whether to update the offsets when nearest ancestor is scrolled. | +| [`handleKeyboardGlobally`](#handleKeyboardGlobally) | boolean | false | Indicates whether to bind the keyboard events to document. | ### Dimensions of Excalidraw @@ -592,6 +593,12 @@ Try out the [Demo](#Demo) to see it in action. Indicates whether Excalidraw should listen for `scroll` event on the nearest scrollable container in the DOM tree and recompute the coordinates (e.g. to correctly handle the cursor) when the component's position changes. You can disable this when you either know this doesn't affect your app or you want to take care of it yourself (calling the [`refresh()`](#ref) method). +### handleKeyboardGlobally + +Indicates whether to bind keyboard events to `document`. Disabled by default, meaning the keyboard events are bound to the Excalidraw component. This allows for multiple Excalidraw components to live on the same page, and ensures that Excalidraw keyboard handling doesn't collide with your app's (or the browser) when the component isn't focused. + +Enable this if you want Excalidraw to handle keyboard even if the component isn't focused (e.g. a user is interacting with the navbar, sidebar, or similar). + ### Extra API's #### `getSceneVersion` diff --git a/src/packages/excalidraw/index.tsx b/src/packages/excalidraw/index.tsx index 3278169e..ffe10635 100644 --- a/src/packages/excalidraw/index.tsx +++ b/src/packages/excalidraw/index.tsx @@ -31,6 +31,7 @@ const Excalidraw = (props: ExcalidrawProps) => { renderCustomStats, onPaste, detectScroll = true, + handleKeyboardGlobally = false, } = props; const canvasActions = props.UIOptions?.canvasActions; @@ -82,6 +83,7 @@ const Excalidraw = (props: ExcalidrawProps) => { UIOptions={UIOptions} onPaste={onPaste} detectScroll={detectScroll} + handleKeyboardGlobally={handleKeyboardGlobally} /> ); diff --git a/src/tests/__snapshots__/dragCreate.test.tsx.snap b/src/tests/__snapshots__/dragCreate.test.tsx.snap index d2721d30..2f7cb643 100644 --- a/src/tests/__snapshots__/dragCreate.test.tsx.snap +++ b/src/tests/__snapshots__/dragCreate.test.tsx.snap @@ -1,8 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`add element to the scene when pointer dragging long enough arrow 1`] = `1`; +exports[`Test dragCreate add element to the scene when pointer dragging long enough arrow 1`] = `1`; -exports[`add element to the scene when pointer dragging long enough arrow 2`] = ` +exports[`Test dragCreate add element to the scene when pointer dragging long enough arrow 2`] = ` Object { "angle": 0, "backgroundColor": "transparent", @@ -43,9 +43,9 @@ Object { } `; -exports[`add element to the scene when pointer dragging long enough diamond 1`] = `1`; +exports[`Test dragCreate add element to the scene when pointer dragging long enough diamond 1`] = `1`; -exports[`add element to the scene when pointer dragging long enough diamond 2`] = ` +exports[`Test dragCreate add element to the scene when pointer dragging long enough diamond 2`] = ` Object { "angle": 0, "backgroundColor": "transparent", @@ -71,9 +71,9 @@ Object { } `; -exports[`add element to the scene when pointer dragging long enough ellipse 1`] = `1`; +exports[`Test dragCreate add element to the scene when pointer dragging long enough ellipse 1`] = `1`; -exports[`add element to the scene when pointer dragging long enough ellipse 2`] = ` +exports[`Test dragCreate add element to the scene when pointer dragging long enough ellipse 2`] = ` Object { "angle": 0, "backgroundColor": "transparent", @@ -99,7 +99,7 @@ Object { } `; -exports[`add element to the scene when pointer dragging long enough line 1`] = ` +exports[`Test dragCreate add element to the scene when pointer dragging long enough line 1`] = ` Object { "angle": 0, "backgroundColor": "transparent", @@ -140,9 +140,9 @@ Object { } `; -exports[`add element to the scene when pointer dragging long enough rectangle 1`] = `1`; +exports[`Test dragCreate add element to the scene when pointer dragging long enough rectangle 1`] = `1`; -exports[`add element to the scene when pointer dragging long enough rectangle 2`] = ` +exports[`Test dragCreate add element to the scene when pointer dragging long enough rectangle 2`] = ` Object { "angle": 0, "backgroundColor": "transparent", diff --git a/src/tests/dragCreate.test.tsx b/src/tests/dragCreate.test.tsx index 895f882d..3af66833 100644 --- a/src/tests/dragCreate.test.tsx +++ b/src/tests/dragCreate.test.tsx @@ -24,276 +24,282 @@ beforeEach(() => { const { h } = window; -describe("add element to the scene when pointer dragging long enough", () => { - it("rectangle", async () => { - const { getByToolName, container } = await render(); - // select tool - const tool = getByToolName("rectangle"); - fireEvent.click(tool); +describe("Test dragCreate", () => { + describe("add element to the scene when pointer dragging long enough", () => { + it("rectangle", async () => { + const { getByToolName, container } = await render(); + // select tool + const tool = getByToolName("rectangle"); + fireEvent.click(tool); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas")!; - // start from (30, 20) - fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + // start from (30, 20) + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); - // move to (60,70) - fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); + // move to (60,70) + fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); - // finish (position does not matter) - fireEvent.pointerUp(canvas); + // finish (position does not matter) + fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(8); - expect(h.state.selectionElement).toBeNull(); + expect(renderScene).toHaveBeenCalledTimes(8); + expect(h.state.selectionElement).toBeNull(); - expect(h.elements.length).toEqual(1); - expect(h.elements[0].type).toEqual("rectangle"); - expect(h.elements[0].x).toEqual(30); - expect(h.elements[0].y).toEqual(20); - expect(h.elements[0].width).toEqual(30); // 60 - 30 - expect(h.elements[0].height).toEqual(50); // 70 - 20 + expect(h.elements.length).toEqual(1); + expect(h.elements[0].type).toEqual("rectangle"); + expect(h.elements[0].x).toEqual(30); + expect(h.elements[0].y).toEqual(20); + expect(h.elements[0].width).toEqual(30); // 60 - 30 + expect(h.elements[0].height).toEqual(50); // 70 - 20 - expect(h.elements.length).toMatchSnapshot(); - h.elements.forEach((element) => expect(element).toMatchSnapshot()); + expect(h.elements.length).toMatchSnapshot(); + h.elements.forEach((element) => expect(element).toMatchSnapshot()); + }); + + it("ellipse", async () => { + const { getByToolName, container } = await render(); + // select tool + const tool = getByToolName("ellipse"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas")!; + + // start from (30, 20) + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + + // move to (60,70) + fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); + + // finish (position does not matter) + fireEvent.pointerUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(8); + expect(h.state.selectionElement).toBeNull(); + + expect(h.elements.length).toEqual(1); + expect(h.elements[0].type).toEqual("ellipse"); + expect(h.elements[0].x).toEqual(30); + expect(h.elements[0].y).toEqual(20); + expect(h.elements[0].width).toEqual(30); // 60 - 30 + expect(h.elements[0].height).toEqual(50); // 70 - 20 + + expect(h.elements.length).toMatchSnapshot(); + h.elements.forEach((element) => expect(element).toMatchSnapshot()); + }); + + it("diamond", async () => { + const { getByToolName, container } = await render(); + // select tool + const tool = getByToolName("diamond"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas")!; + + // start from (30, 20) + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + + // move to (60,70) + fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); + + // finish (position does not matter) + fireEvent.pointerUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(8); + expect(h.state.selectionElement).toBeNull(); + + expect(h.elements.length).toEqual(1); + expect(h.elements[0].type).toEqual("diamond"); + expect(h.elements[0].x).toEqual(30); + expect(h.elements[0].y).toEqual(20); + expect(h.elements[0].width).toEqual(30); // 60 - 30 + expect(h.elements[0].height).toEqual(50); // 70 - 20 + + expect(h.elements.length).toMatchSnapshot(); + h.elements.forEach((element) => expect(element).toMatchSnapshot()); + }); + + it("arrow", async () => { + const { getByToolName, container } = await render(); + // select tool + const tool = getByToolName("arrow"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas")!; + + // start from (30, 20) + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + + // move to (60,70) + fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); + + // finish (position does not matter) + fireEvent.pointerUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(8); + expect(h.state.selectionElement).toBeNull(); + + expect(h.elements.length).toEqual(1); + + const element = h.elements[0] as ExcalidrawLinearElement; + + expect(element.type).toEqual("arrow"); + expect(element.x).toEqual(30); + expect(element.y).toEqual(20); + expect(element.points.length).toEqual(2); + expect(element.points[0]).toEqual([0, 0]); + expect(element.points[1]).toEqual([30, 50]); // (60 - 30, 70 - 20) + + expect(h.elements.length).toMatchSnapshot(); + h.elements.forEach((element) => expect(element).toMatchSnapshot()); + }); + + it("line", async () => { + const { getByToolName, container } = await render(); + // select tool + const tool = getByToolName("line"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas")!; + + // start from (30, 20) + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + + // move to (60,70) + fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); + + // finish (position does not matter) + fireEvent.pointerUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(8); + expect(h.state.selectionElement).toBeNull(); + + expect(h.elements.length).toEqual(1); + + const element = h.elements[0] as ExcalidrawLinearElement; + + expect(element.type).toEqual("line"); + expect(element.x).toEqual(30); + expect(element.y).toEqual(20); + expect(element.points.length).toEqual(2); + expect(element.points[0]).toEqual([0, 0]); + expect(element.points[1]).toEqual([30, 50]); // (60 - 30, 70 - 20) + + h.elements.forEach((element) => expect(element).toMatchSnapshot()); + }); }); - it("ellipse", async () => { - const { getByToolName, container } = await render(); - // select tool - const tool = getByToolName("ellipse"); - fireEvent.click(tool); + describe("do not add element to the scene if size is too small", () => { + beforeAll(() => { + mockBoundingClientRect(); + }); + afterAll(() => { + restoreOriginalGetBoundingClientRect(); + }); - const canvas = container.querySelector("canvas")!; + it("rectangle", async () => { + const { getByToolName, container } = await render(); + // select tool + const tool = getByToolName("rectangle"); + fireEvent.click(tool); - // start from (30, 20) - fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + const canvas = container.querySelector("canvas")!; - // move to (60,70) - fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); + // start from (30, 20) + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); - // finish (position does not matter) - fireEvent.pointerUp(canvas); + // finish (position does not matter) + fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(8); - expect(h.state.selectionElement).toBeNull(); + expect(renderScene).toHaveBeenCalledTimes(6); + expect(h.state.selectionElement).toBeNull(); + expect(h.elements.length).toEqual(0); + }); - expect(h.elements.length).toEqual(1); - expect(h.elements[0].type).toEqual("ellipse"); - expect(h.elements[0].x).toEqual(30); - expect(h.elements[0].y).toEqual(20); - expect(h.elements[0].width).toEqual(30); // 60 - 30 - expect(h.elements[0].height).toEqual(50); // 70 - 20 + it("ellipse", async () => { + const { getByToolName, container } = await render(); + // select tool + const tool = getByToolName("ellipse"); + fireEvent.click(tool); - expect(h.elements.length).toMatchSnapshot(); - h.elements.forEach((element) => expect(element).toMatchSnapshot()); - }); + const canvas = container.querySelector("canvas")!; - it("diamond", async () => { - const { getByToolName, container } = await render(); - // select tool - const tool = getByToolName("diamond"); - fireEvent.click(tool); + // start from (30, 20) + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); - const canvas = container.querySelector("canvas")!; + // finish (position does not matter) + fireEvent.pointerUp(canvas); - // start from (30, 20) - fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + expect(renderScene).toHaveBeenCalledTimes(6); + expect(h.state.selectionElement).toBeNull(); + expect(h.elements.length).toEqual(0); + }); - // move to (60,70) - fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); + it("diamond", async () => { + const { getByToolName, container } = await render(); + // select tool + const tool = getByToolName("diamond"); + fireEvent.click(tool); - // finish (position does not matter) - fireEvent.pointerUp(canvas); + const canvas = container.querySelector("canvas")!; - expect(renderScene).toHaveBeenCalledTimes(8); - expect(h.state.selectionElement).toBeNull(); + // start from (30, 20) + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); - expect(h.elements.length).toEqual(1); - expect(h.elements[0].type).toEqual("diamond"); - expect(h.elements[0].x).toEqual(30); - expect(h.elements[0].y).toEqual(20); - expect(h.elements[0].width).toEqual(30); // 60 - 30 - expect(h.elements[0].height).toEqual(50); // 70 - 20 + // finish (position does not matter) + fireEvent.pointerUp(canvas); - expect(h.elements.length).toMatchSnapshot(); - h.elements.forEach((element) => expect(element).toMatchSnapshot()); - }); + expect(renderScene).toHaveBeenCalledTimes(6); + expect(h.state.selectionElement).toBeNull(); + expect(h.elements.length).toEqual(0); + }); - it("arrow", async () => { - const { getByToolName, container } = await render(); - // select tool - const tool = getByToolName("arrow"); - fireEvent.click(tool); + it("arrow", async () => { + const { getByToolName, container } = await render(); + // select tool + const tool = getByToolName("arrow"); + fireEvent.click(tool); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas")!; - // start from (30, 20) - fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + // start from (30, 20) + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); - // move to (60,70) - fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); + // finish (position does not matter) + fireEvent.pointerUp(canvas); - // finish (position does not matter) - fireEvent.pointerUp(canvas); + // we need to finalize it because arrows and lines enter multi-mode + fireEvent.keyDown(document, { + key: KEYS.ENTER, + }); - expect(renderScene).toHaveBeenCalledTimes(8); - expect(h.state.selectionElement).toBeNull(); + expect(renderScene).toHaveBeenCalledTimes(7); + expect(h.state.selectionElement).toBeNull(); + expect(h.elements.length).toEqual(0); + }); - expect(h.elements.length).toEqual(1); + it("line", async () => { + const { getByToolName, container } = await render(); + // select tool + const tool = getByToolName("line"); + fireEvent.click(tool); - const element = h.elements[0] as ExcalidrawLinearElement; + const canvas = container.querySelector("canvas")!; - expect(element.type).toEqual("arrow"); - expect(element.x).toEqual(30); - expect(element.y).toEqual(20); - expect(element.points.length).toEqual(2); - expect(element.points[0]).toEqual([0, 0]); - expect(element.points[1]).toEqual([30, 50]); // (60 - 30, 70 - 20) + // start from (30, 20) + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); - expect(h.elements.length).toMatchSnapshot(); - h.elements.forEach((element) => expect(element).toMatchSnapshot()); - }); + // finish (position does not matter) + fireEvent.pointerUp(canvas); - it("line", async () => { - const { getByToolName, container } = await render(); - // select tool - const tool = getByToolName("line"); - fireEvent.click(tool); + // we need to finalize it because arrows and lines enter multi-mode + fireEvent.keyDown(document, { + key: KEYS.ENTER, + }); - const canvas = container.querySelector("canvas")!; - - // start from (30, 20) - fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); - - // move to (60,70) - fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); - - // finish (position does not matter) - fireEvent.pointerUp(canvas); - - expect(renderScene).toHaveBeenCalledTimes(8); - expect(h.state.selectionElement).toBeNull(); - - expect(h.elements.length).toEqual(1); - - const element = h.elements[0] as ExcalidrawLinearElement; - - expect(element.type).toEqual("line"); - expect(element.x).toEqual(30); - expect(element.y).toEqual(20); - expect(element.points.length).toEqual(2); - expect(element.points[0]).toEqual([0, 0]); - expect(element.points[1]).toEqual([30, 50]); // (60 - 30, 70 - 20) - - h.elements.forEach((element) => expect(element).toMatchSnapshot()); - }); -}); - -describe("do not add element to the scene if size is too small", () => { - beforeAll(() => { - mockBoundingClientRect(); - }); - afterAll(() => { - restoreOriginalGetBoundingClientRect(); - }); - - it("rectangle", async () => { - const { getByToolName, container } = await render(); - // select tool - const tool = getByToolName("rectangle"); - fireEvent.click(tool); - - const canvas = container.querySelector("canvas")!; - - // start from (30, 20) - fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); - - // finish (position does not matter) - fireEvent.pointerUp(canvas); - - expect(renderScene).toHaveBeenCalledTimes(6); - expect(h.state.selectionElement).toBeNull(); - expect(h.elements.length).toEqual(0); - }); - - it("ellipse", async () => { - const { getByToolName, container } = await render(); - // select tool - const tool = getByToolName("ellipse"); - fireEvent.click(tool); - - const canvas = container.querySelector("canvas")!; - - // start from (30, 20) - fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); - - // finish (position does not matter) - fireEvent.pointerUp(canvas); - - expect(renderScene).toHaveBeenCalledTimes(6); - expect(h.state.selectionElement).toBeNull(); - expect(h.elements.length).toEqual(0); - }); - - it("diamond", async () => { - const { getByToolName, container } = await render(); - // select tool - const tool = getByToolName("diamond"); - fireEvent.click(tool); - - const canvas = container.querySelector("canvas")!; - - // start from (30, 20) - fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); - - // finish (position does not matter) - fireEvent.pointerUp(canvas); - - expect(renderScene).toHaveBeenCalledTimes(6); - expect(h.state.selectionElement).toBeNull(); - expect(h.elements.length).toEqual(0); - }); - - it("arrow", async () => { - const { getByToolName, container } = await render(); - // select tool - const tool = getByToolName("arrow"); - fireEvent.click(tool); - - const canvas = container.querySelector("canvas")!; - - // start from (30, 20) - fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); - - // finish (position does not matter) - fireEvent.pointerUp(canvas); - - // we need to finalize it because arrows and lines enter multi-mode - fireEvent.keyDown(document, { key: KEYS.ENTER }); - - expect(renderScene).toHaveBeenCalledTimes(7); - expect(h.state.selectionElement).toBeNull(); - expect(h.elements.length).toEqual(0); - }); - - it("line", async () => { - const { getByToolName, container } = await render(); - // select tool - const tool = getByToolName("line"); - fireEvent.click(tool); - - const canvas = container.querySelector("canvas")!; - - // start from (30, 20) - fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); - - // finish (position does not matter) - fireEvent.pointerUp(canvas); - - // we need to finalize it because arrows and lines enter multi-mode - fireEvent.keyDown(document, { key: KEYS.ENTER }); - - expect(renderScene).toHaveBeenCalledTimes(7); - expect(h.state.selectionElement).toBeNull(); - expect(h.elements.length).toEqual(0); + expect(renderScene).toHaveBeenCalledTimes(7); + expect(h.state.selectionElement).toBeNull(); + expect(h.elements.length).toEqual(0); + }); }); }); diff --git a/src/tests/multiPointCreate.test.tsx b/src/tests/multiPointCreate.test.tsx index 748f8807..d3e40767 100644 --- a/src/tests/multiPointCreate.test.tsx +++ b/src/tests/multiPointCreate.test.tsx @@ -99,7 +99,9 @@ describe("multi point mode in linear elements", () => { // done fireEvent.pointerDown(canvas); fireEvent.pointerUp(canvas); - fireEvent.keyDown(document, { key: KEYS.ENTER }); + fireEvent.keyDown(document, { + key: KEYS.ENTER, + }); expect(renderScene).toHaveBeenCalledTimes(14); expect(h.elements.length).toEqual(1); @@ -140,7 +142,9 @@ describe("multi point mode in linear elements", () => { // done fireEvent.pointerDown(canvas); fireEvent.pointerUp(canvas); - fireEvent.keyDown(document, { key: KEYS.ENTER }); + fireEvent.keyDown(document, { + key: KEYS.ENTER, + }); expect(renderScene).toHaveBeenCalledTimes(14); expect(h.elements.length).toEqual(1); diff --git a/src/tests/regressionTests.test.tsx b/src/tests/regressionTests.test.tsx index 9c44a497..3aca8c38 100644 --- a/src/tests/regressionTests.test.tsx +++ b/src/tests/regressionTests.test.tsx @@ -413,11 +413,23 @@ describe("regression tests", () => { it("zoom hotkeys", () => { expect(h.state.zoom.value).toBe(1); - fireEvent.keyDown(document, { code: CODES.EQUAL, ctrlKey: true }); - fireEvent.keyUp(document, { code: CODES.EQUAL, ctrlKey: true }); + fireEvent.keyDown(document, { + code: CODES.EQUAL, + ctrlKey: true, + }); + fireEvent.keyUp(document, { + code: CODES.EQUAL, + ctrlKey: true, + }); expect(h.state.zoom.value).toBeGreaterThan(1); - fireEvent.keyDown(document, { code: CODES.MINUS, ctrlKey: true }); - fireEvent.keyUp(document, { code: CODES.MINUS, ctrlKey: true }); + fireEvent.keyDown(document, { + code: CODES.MINUS, + ctrlKey: true, + }); + fireEvent.keyUp(document, { + code: CODES.MINUS, + ctrlKey: true, + }); expect(h.state.zoom.value).toBe(1); }); diff --git a/src/tests/selection.test.tsx b/src/tests/selection.test.tsx index b882713a..6fa1bdda 100644 --- a/src/tests/selection.test.tsx +++ b/src/tests/selection.test.tsx @@ -100,7 +100,9 @@ describe("select single element on the scene", () => { fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); fireEvent.pointerUp(canvas); - fireEvent.keyDown(document, { key: KEYS.ESCAPE }); + fireEvent.keyDown(document, { + key: KEYS.ESCAPE, + }); } const tool = getByToolName("selection"); @@ -127,7 +129,9 @@ describe("select single element on the scene", () => { fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); fireEvent.pointerUp(canvas); - fireEvent.keyDown(document, { key: KEYS.ESCAPE }); + fireEvent.keyDown(document, { + key: KEYS.ESCAPE, + }); } const tool = getByToolName("selection"); @@ -154,7 +158,9 @@ describe("select single element on the scene", () => { fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); fireEvent.pointerUp(canvas); - fireEvent.keyDown(document, { key: KEYS.ESCAPE }); + fireEvent.keyDown(document, { + key: KEYS.ESCAPE, + }); } const tool = getByToolName("selection"); @@ -181,7 +187,9 @@ describe("select single element on the scene", () => { fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); fireEvent.pointerUp(canvas); - fireEvent.keyDown(document, { key: KEYS.ESCAPE }); + fireEvent.keyDown(document, { + key: KEYS.ESCAPE, + }); } /* @@ -220,7 +228,9 @@ describe("select single element on the scene", () => { fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); fireEvent.pointerUp(canvas); - fireEvent.keyDown(document, { key: KEYS.ESCAPE }); + fireEvent.keyDown(document, { + key: KEYS.ESCAPE, + }); } /* diff --git a/src/types.ts b/src/types.ts index e2d52f82..7b95f383 100644 --- a/src/types.ts +++ b/src/types.ts @@ -196,6 +196,7 @@ export interface ExcalidrawProps { ) => JSX.Element; UIOptions?: UIOptions; detectScroll?: boolean; + handleKeyboardGlobally?: boolean; } export type SceneData = { @@ -230,4 +231,5 @@ export type AppProps = ExcalidrawProps & { canvasActions: Required; }; detectScroll: boolean; + handleKeyboardGlobally: boolean; }; diff --git a/src/utils.ts b/src/utils.ts index 68c6e19b..58ce9b28 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -427,3 +427,14 @@ export const getNearestScrollableContainer = ( } return document; }; + +export const focusNearestParent = (element: HTMLInputElement) => { + let parent = element.parentElement; + while (parent) { + if (parent.tabIndex > -1) { + parent.focus(); + return; + } + parent = parent.parentElement; + } +};