feat: Bind keyboard events to the current excalidraw container and add handleKeyboardGlobally prop to allow host to bind to document (#3430)
* fix: Bind keyboard events to excalidraw container * fix cases around blurring * fix modal rendering so keyboard shortcuts work on modal as well * Revert "fix modal rendering so keyboard shortcuts work on modal as well" This reverts commit 2c8ec6be8eff7d308591467fe2c33cfbca16138f. * Attach keyboard event in react way so we need not handle portals separately (modals) * dnt propagate esc event when modal shown * focus the container when help dialog closed with shift+? * focus the help icon when help dialog on close triggered * move focusNearestTabbableParent to util * rename util to focusNearestParent and remove outline from excal and modal * Add prop bindKeyGlobally to decide if keyboard events should be binded to document and allow it in excal app, revert tests * fix * focus container after installing library, reset library and closing error dialog * fix tests and create util to focus container * Add excalidraw-container class to focus on the container * pass focus container to library to focus current instance of excal * update docs * remove util as it wont be used anywhere * fix propagation not being stopped for React keyboard handling * tweak reamde Co-authored-by: David Luzar <luzar.david@gmail.com> * tweak changelog * rename prop to handleKeyboardGlobally Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
153ca6a7c6
commit
d126d04d17
@ -18,7 +18,7 @@ import { isBindingElement } from "../element/typeChecks";
|
|||||||
|
|
||||||
export const actionFinalize = register({
|
export const actionFinalize = register({
|
||||||
name: "finalize",
|
name: "finalize",
|
||||||
perform: (elements, appState, _, { canvas }) => {
|
perform: (elements, appState, _, { canvas, focusContainer }) => {
|
||||||
if (appState.editingLinearElement) {
|
if (appState.editingLinearElement) {
|
||||||
const {
|
const {
|
||||||
elementId,
|
elementId,
|
||||||
@ -51,7 +51,7 @@ export const actionFinalize = register({
|
|||||||
|
|
||||||
let newElements = elements;
|
let newElements = elements;
|
||||||
if (window.document.activeElement instanceof HTMLElement) {
|
if (window.document.activeElement instanceof HTMLElement) {
|
||||||
window.document.activeElement.blur();
|
focusContainer();
|
||||||
}
|
}
|
||||||
|
|
||||||
const multiPointElement = appState.multiElement
|
const multiPointElement = appState.multiElement
|
||||||
|
@ -70,7 +70,10 @@ export const actionFullScreen = register({
|
|||||||
|
|
||||||
export const actionShortcuts = register({
|
export const actionShortcuts = register({
|
||||||
name: "toggleShortcuts",
|
name: "toggleShortcuts",
|
||||||
perform: (_elements, appState) => {
|
perform: (_elements, appState, _, { focusContainer }) => {
|
||||||
|
if (appState.showHelpDialog) {
|
||||||
|
focusContainer();
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
|
@ -12,7 +12,11 @@ import { MODES } from "../constants";
|
|||||||
|
|
||||||
// This is the <App> component, but for now we don't care about anything but its
|
// This is the <App> component, but for now we don't care about anything but its
|
||||||
// `canvas` state.
|
// `canvas` state.
|
||||||
type App = { canvas: HTMLCanvasElement | null; props: AppProps };
|
type App = {
|
||||||
|
canvas: HTMLCanvasElement | null;
|
||||||
|
focusContainer: () => void;
|
||||||
|
props: AppProps;
|
||||||
|
};
|
||||||
|
|
||||||
export class ActionManager implements ActionsManagerInterface {
|
export class ActionManager implements ActionsManagerInterface {
|
||||||
actions = {} as ActionsManagerInterface["actions"];
|
actions = {} as ActionsManagerInterface["actions"];
|
||||||
@ -51,7 +55,7 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
actions.forEach((action) => this.registerAction(action));
|
actions.forEach((action) => this.registerAction(action));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown(event: KeyboardEvent) {
|
handleKeyDown(event: React.KeyboardEvent | KeyboardEvent) {
|
||||||
const canvasActions = this.app.props.UIOptions.canvasActions;
|
const canvasActions = this.app.props.UIOptions.canvasActions;
|
||||||
const data = Object.values(this.actions)
|
const data = Object.values(this.actions)
|
||||||
.sort((a, b) => (b.keyPriority || 0) - (a.keyPriority || 0))
|
.sort((a, b) => (b.keyPriority || 0) - (a.keyPriority || 0))
|
||||||
|
@ -15,11 +15,13 @@ export type ActionResult =
|
|||||||
}
|
}
|
||||||
| false;
|
| false;
|
||||||
|
|
||||||
|
type AppAPI = { canvas: HTMLCanvasElement | null; focusContainer(): void };
|
||||||
|
|
||||||
type ActionFn = (
|
type ActionFn = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
formData: any,
|
formData: any,
|
||||||
app: { canvas: HTMLCanvasElement | null },
|
app: AppAPI,
|
||||||
) => ActionResult | Promise<ActionResult>;
|
) => ActionResult | Promise<ActionResult>;
|
||||||
|
|
||||||
export type UpdaterFn = (res: ActionResult) => void;
|
export type UpdaterFn = (res: ActionResult) => void;
|
||||||
@ -105,7 +107,7 @@ export interface Action {
|
|||||||
perform: ActionFn;
|
perform: ActionFn;
|
||||||
keyPriority?: number;
|
keyPriority?: number;
|
||||||
keyTest?: (
|
keyTest?: (
|
||||||
event: KeyboardEvent,
|
event: React.KeyboardEvent | KeyboardEvent,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
) => boolean;
|
) => boolean;
|
||||||
@ -120,6 +122,6 @@ export interface Action {
|
|||||||
export interface ActionsManagerInterface {
|
export interface ActionsManagerInterface {
|
||||||
actions: Record<ActionName, Action>;
|
actions: Record<ActionName, Action>;
|
||||||
registerAction: (action: Action) => void;
|
registerAction: (action: Action) => void;
|
||||||
handleKeyDown: (event: KeyboardEvent) => boolean;
|
handleKeyDown: (event: React.KeyboardEvent | KeyboardEvent) => boolean;
|
||||||
renderAction: (name: ActionName) => React.ReactElement | null;
|
renderAction: (name: ActionName) => React.ReactElement | null;
|
||||||
}
|
}
|
||||||
|
@ -445,12 +445,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx("excalidraw", {
|
className={clsx("excalidraw excalidraw-container", {
|
||||||
"excalidraw--view-mode": viewModeEnabled,
|
"excalidraw--view-mode": viewModeEnabled,
|
||||||
"excalidraw--mobile": this.isMobile,
|
"excalidraw--mobile": this.isMobile,
|
||||||
})}
|
})}
|
||||||
ref={this.excalidrawContainerRef}
|
ref={this.excalidrawContainerRef}
|
||||||
onDrop={this.handleAppOnDrop}
|
onDrop={this.handleAppOnDrop}
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={
|
||||||
|
this.props.handleKeyboardGlobally ? undefined : this.onKeyDown
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<IsMobileContext.Provider value={this.isMobile}>
|
<IsMobileContext.Provider value={this.isMobile}>
|
||||||
<LayerUI
|
<LayerUI
|
||||||
@ -485,6 +489,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
libraryReturnUrl={this.props.libraryReturnUrl}
|
libraryReturnUrl={this.props.libraryReturnUrl}
|
||||||
UIOptions={this.props.UIOptions}
|
UIOptions={this.props.UIOptions}
|
||||||
|
focusContainer={this.focusContainer}
|
||||||
/>
|
/>
|
||||||
<div className="excalidraw-textEditorContainer" />
|
<div className="excalidraw-textEditorContainer" />
|
||||||
<div className="excalidraw-contextMenuContainer" />
|
<div className="excalidraw-contextMenuContainer" />
|
||||||
@ -509,6 +514,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public focusContainer = () => {
|
||||||
|
this.excalidrawContainerRef.current?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
public getSceneElementsIncludingDeleted = () => {
|
public getSceneElementsIncludingDeleted = () => {
|
||||||
return this.scene.getElementsIncludingDeleted();
|
return this.scene.getElementsIncludingDeleted();
|
||||||
};
|
};
|
||||||
@ -655,6 +664,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.alert(t("alerts.errorLoadingLibrary"));
|
window.alert(t("alerts.errorLoadingLibrary"));
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
this.focusContainer();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -795,6 +806,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scene.addCallback(this.onSceneUpdated);
|
this.scene.addCallback(this.onSceneUpdated);
|
||||||
this.addEventListeners();
|
this.addEventListeners();
|
||||||
|
|
||||||
|
if (this.excalidrawContainerRef.current) {
|
||||||
|
this.focusContainer();
|
||||||
|
}
|
||||||
|
|
||||||
if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) {
|
if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) {
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
// compute isMobile state
|
// compute isMobile state
|
||||||
@ -854,7 +869,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
EVENT.SCROLL,
|
EVENT.SCROLL,
|
||||||
this.onScroll,
|
this.onScroll,
|
||||||
);
|
);
|
||||||
|
|
||||||
document.removeEventListener(EVENT.KEYDOWN, this.onKeyDown, false);
|
document.removeEventListener(EVENT.KEYDOWN, this.onKeyDown, false);
|
||||||
document.removeEventListener(
|
document.removeEventListener(
|
||||||
EVENT.MOUSE_MOVE,
|
EVENT.MOUSE_MOVE,
|
||||||
@ -890,7 +904,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
private addEventListeners() {
|
private addEventListeners() {
|
||||||
this.removeEventListeners();
|
this.removeEventListeners();
|
||||||
document.addEventListener(EVENT.COPY, this.onCopy);
|
document.addEventListener(EVENT.COPY, this.onCopy);
|
||||||
|
if (this.props.handleKeyboardGlobally) {
|
||||||
document.addEventListener(EVENT.KEYDOWN, this.onKeyDown, false);
|
document.addEventListener(EVENT.KEYDOWN, this.onKeyDown, false);
|
||||||
|
}
|
||||||
document.addEventListener(EVENT.KEYUP, this.onKeyUp, { passive: true });
|
document.addEventListener(EVENT.KEYUP, this.onKeyUp, { passive: true });
|
||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
EVENT.MOUSE_MOVE,
|
EVENT.MOUSE_MOVE,
|
||||||
@ -1434,7 +1450,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
// Input handling
|
// Input handling
|
||||||
|
|
||||||
private onKeyDown = withBatchedUpdates((event: KeyboardEvent) => {
|
private onKeyDown = withBatchedUpdates(
|
||||||
|
(event: React.KeyboardEvent | KeyboardEvent) => {
|
||||||
// normalize `event.key` when CapsLock is pressed #2372
|
// normalize `event.key` when CapsLock is pressed #2372
|
||||||
if (
|
if (
|
||||||
"Proxy" in window &&
|
"Proxy" in window &&
|
||||||
@ -1492,7 +1509,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (isArrowKey(event.key)) {
|
if (isArrowKey(event.key)) {
|
||||||
const step =
|
const step =
|
||||||
(this.state.gridSize &&
|
(this.state.gridSize &&
|
||||||
(event.shiftKey ? ELEMENT_TRANSLATE_AMOUNT : this.state.gridSize)) ||
|
(event.shiftKey
|
||||||
|
? ELEMENT_TRANSLATE_AMOUNT
|
||||||
|
: this.state.gridSize)) ||
|
||||||
(event.shiftKey
|
(event.shiftKey
|
||||||
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
|
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
|
||||||
: ELEMENT_TRANSLATE_AMOUNT);
|
: ELEMENT_TRANSLATE_AMOUNT);
|
||||||
@ -1579,7 +1598,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
isHoldingSpace = true;
|
isHoldingSpace = true;
|
||||||
setCursor(this.canvas, CURSOR_TYPE.GRABBING);
|
setCursor(this.canvas, CURSOR_TYPE.GRABBING);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => {
|
private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => {
|
||||||
if (event.key === KEYS.SPACE) {
|
if (event.key === KEYS.SPACE) {
|
||||||
@ -1615,7 +1635,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
setCursorForShape(this.canvas, elementType);
|
setCursorForShape(this.canvas, elementType);
|
||||||
}
|
}
|
||||||
if (isToolIcon(document.activeElement)) {
|
if (isToolIcon(document.activeElement)) {
|
||||||
document.activeElement.blur();
|
this.focusContainer();
|
||||||
}
|
}
|
||||||
if (!isLinearElementType(elementType)) {
|
if (!isLinearElementType(elementType)) {
|
||||||
this.setState({ suggestedBindings: [] });
|
this.setState({ suggestedBindings: [] });
|
||||||
@ -1745,6 +1765,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (this.state.elementLocked) {
|
if (this.state.elementLocked) {
|
||||||
setCursorForShape(this.canvas, this.state.elementType);
|
setCursorForShape(this.canvas, this.state.elementType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.focusContainer();
|
||||||
}),
|
}),
|
||||||
element,
|
element,
|
||||||
});
|
});
|
||||||
|
@ -115,6 +115,7 @@ const Picker = ({
|
|||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
event.nativeEvent.stopImmediatePropagation();
|
event.nativeEvent.stopImmediatePropagation();
|
||||||
|
event.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -18,7 +18,6 @@ export const Dialog = (props: {
|
|||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!islandNode) {
|
if (!islandNode) {
|
||||||
return;
|
return;
|
||||||
|
@ -18,6 +18,7 @@ export const ErrorDialog = ({
|
|||||||
if (onClose) {
|
if (onClose) {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
document.querySelector<HTMLElement>(".excalidraw-container")?.focus();
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -88,6 +88,7 @@ function Picker<T>({
|
|||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
event.nativeEvent.stopImmediatePropagation();
|
event.nativeEvent.stopImmediatePropagation();
|
||||||
|
event.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -72,6 +72,7 @@ interface LayerUIProps {
|
|||||||
viewModeEnabled: boolean;
|
viewModeEnabled: boolean;
|
||||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||||
UIOptions: AppProps["UIOptions"];
|
UIOptions: AppProps["UIOptions"];
|
||||||
|
focusContainer: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useOnClickOutside = (
|
const useOnClickOutside = (
|
||||||
@ -111,6 +112,7 @@ const LibraryMenuItems = ({
|
|||||||
setAppState,
|
setAppState,
|
||||||
setLibraryItems,
|
setLibraryItems,
|
||||||
libraryReturnUrl,
|
libraryReturnUrl,
|
||||||
|
focusContainer,
|
||||||
}: {
|
}: {
|
||||||
library: LibraryItems;
|
library: LibraryItems;
|
||||||
pendingElements: LibraryItem;
|
pendingElements: LibraryItem;
|
||||||
@ -120,6 +122,7 @@ const LibraryMenuItems = ({
|
|||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
setLibraryItems: (library: LibraryItems) => void;
|
setLibraryItems: (library: LibraryItems) => void;
|
||||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||||
|
focusContainer: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const numCells = library.length + (pendingElements.length > 0 ? 1 : 0);
|
const numCells = library.length + (pendingElements.length > 0 ? 1 : 0);
|
||||||
@ -178,6 +181,7 @@ const LibraryMenuItems = ({
|
|||||||
if (window.confirm(t("alerts.resetLibrary"))) {
|
if (window.confirm(t("alerts.resetLibrary"))) {
|
||||||
Library.resetLibrary();
|
Library.resetLibrary();
|
||||||
setLibraryItems([]);
|
setLibraryItems([]);
|
||||||
|
focusContainer();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -242,6 +246,7 @@ const LibraryMenu = ({
|
|||||||
onAddToLibrary,
|
onAddToLibrary,
|
||||||
setAppState,
|
setAppState,
|
||||||
libraryReturnUrl,
|
libraryReturnUrl,
|
||||||
|
focusContainer,
|
||||||
}: {
|
}: {
|
||||||
pendingElements: LibraryItem;
|
pendingElements: LibraryItem;
|
||||||
onClickOutside: (event: MouseEvent) => void;
|
onClickOutside: (event: MouseEvent) => void;
|
||||||
@ -249,6 +254,7 @@ const LibraryMenu = ({
|
|||||||
onAddToLibrary: () => void;
|
onAddToLibrary: () => void;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||||
|
focusContainer: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
useOnClickOutside(ref, (event) => {
|
useOnClickOutside(ref, (event) => {
|
||||||
@ -322,6 +328,7 @@ const LibraryMenu = ({
|
|||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
setLibraryItems={setLibraryItems}
|
setLibraryItems={setLibraryItems}
|
||||||
libraryReturnUrl={libraryReturnUrl}
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
|
focusContainer={focusContainer}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Island>
|
</Island>
|
||||||
@ -347,6 +354,7 @@ const LayerUI = ({
|
|||||||
viewModeEnabled,
|
viewModeEnabled,
|
||||||
libraryReturnUrl,
|
libraryReturnUrl,
|
||||||
UIOptions,
|
UIOptions,
|
||||||
|
focusContainer,
|
||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
@ -517,6 +525,7 @@ const LayerUI = ({
|
|||||||
onAddToLibrary={deselectItems}
|
onAddToLibrary={deselectItems}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
libraryReturnUrl={libraryReturnUrl}
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
|
focusContainer={focusContainer}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
@ -660,7 +669,15 @@ const LayerUI = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{appState.showHelpDialog && (
|
{appState.showHelpDialog && (
|
||||||
<HelpDialog onClose={() => setAppState({ showHelpDialog: false })} />
|
<HelpDialog
|
||||||
|
onClose={() => {
|
||||||
|
const helpIcon = document.querySelector(
|
||||||
|
".help-icon",
|
||||||
|
)! as HTMLElement;
|
||||||
|
helpIcon.focus();
|
||||||
|
setAppState({ showHelpDialog: false });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{appState.pasteDialog.shown && (
|
{appState.pasteDialog.shown && (
|
||||||
<PasteChartDialog
|
<PasteChartDialog
|
||||||
|
@ -52,6 +52,10 @@
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
@include isMobile {
|
@include isMobile {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
@ -22,6 +22,7 @@ export const Modal = (props: {
|
|||||||
const handleKeydown = (event: React.KeyboardEvent) => {
|
const handleKeydown = (event: React.KeyboardEvent) => {
|
||||||
if (event.key === KEYS.ESCAPE) {
|
if (event.key === KEYS.ESCAPE) {
|
||||||
event.nativeEvent.stopImmediatePropagation();
|
event.nativeEvent.stopImmediatePropagation();
|
||||||
|
event.stopPropagation();
|
||||||
props.onCloseRequest();
|
props.onCloseRequest();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -38,6 +39,7 @@ export const Modal = (props: {
|
|||||||
<div
|
<div
|
||||||
className="Modal__content"
|
className="Modal__content"
|
||||||
style={{ "--max-width": `${props.maxWidth}px` }}
|
style={{ "--max-width": `${props.maxWidth}px` }}
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import "./TextInput.scss";
|
import "./TextInput.scss";
|
||||||
|
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
|
import { focusNearestParent } from "../utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string;
|
value: string;
|
||||||
@ -17,6 +18,7 @@ export class ProjectName extends Component<Props, State> {
|
|||||||
fileName: this.props.value,
|
fileName: this.props.value,
|
||||||
};
|
};
|
||||||
private handleBlur = (event: any) => {
|
private handleBlur = (event: any) => {
|
||||||
|
focusNearestParent(event.target);
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
if (value !== this.props.value) {
|
if (value !== this.props.value) {
|
||||||
this.props.onChange(value);
|
this.props.onChange(value);
|
||||||
|
@ -19,6 +19,10 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
// serves 2 purposes:
|
// serves 2 purposes:
|
||||||
// 1. prevent selecting text outside the component when double-clicking or
|
// 1. prevent selecting text outside the component when double-clicking or
|
||||||
// dragging inside it (e.g. on canvas)
|
// dragging inside it (e.g. on canvas)
|
||||||
|
@ -159,11 +159,14 @@ export const textWysiwyg = ({
|
|||||||
// so that we don't need to create separate a callback for event handlers
|
// so that we don't need to create separate a callback for event handlers
|
||||||
let submittedViaKeyboard = false;
|
let submittedViaKeyboard = false;
|
||||||
const handleSubmit = () => {
|
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({
|
onSubmit({
|
||||||
text: normalizeText(editable.value),
|
text: normalizeText(editable.value),
|
||||||
viaKeyboard: submittedViaKeyboard,
|
viaKeyboard: submittedViaKeyboard,
|
||||||
});
|
});
|
||||||
cleanup();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
|
@ -324,6 +324,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
langCode={langCode}
|
langCode={langCode}
|
||||||
renderCustomStats={renderCustomStats}
|
renderCustomStats={renderCustomStats}
|
||||||
detectScroll={false}
|
detectScroll={false}
|
||||||
|
handleKeyboardGlobally={true}
|
||||||
/>
|
/>
|
||||||
{excalidrawAPI && <CollabWrapper excalidrawAPI={excalidrawAPI} />}
|
{excalidrawAPI && <CollabWrapper excalidrawAPI={excalidrawAPI} />}
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
|
@ -15,6 +15,14 @@ Please add the latest change on the top under the correct section.
|
|||||||
|
|
||||||
## Excalidraw API
|
## 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`.
|
- 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).
|
- Add `onPaste` prop to handle custom clipboard behaviours [#3420](https://github.com/excalidraw/excalidraw/pull/3420).
|
||||||
|
|
||||||
|
@ -366,6 +366,7 @@ To view the full example visit :point_down:
|
|||||||
| [`UIOptions`](#UIOptions) | <pre>{ canvasActions: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L208"> CanvasActions<a/> }</pre> | [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) |
|
| [`UIOptions`](#UIOptions) | <pre>{ canvasActions: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L208"> CanvasActions<a/> }</pre> | [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) | <pre>(data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/clipboard.ts#L17">ClipboardData</a>, event: ClipboardEvent | null) => boolean</pre> | | Callback to be triggered if passed when the something is pasted in to the scene |
|
| [`onPaste`](#onPaste) | <pre>(data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/clipboard.ts#L17">ClipboardData</a>, event: ClipboardEvent | null) => boolean</pre> | | 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. |
|
| [`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
|
### 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).
|
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
|
### Extra API's
|
||||||
|
|
||||||
#### `getSceneVersion`
|
#### `getSceneVersion`
|
||||||
|
@ -31,6 +31,7 @@ const Excalidraw = (props: ExcalidrawProps) => {
|
|||||||
renderCustomStats,
|
renderCustomStats,
|
||||||
onPaste,
|
onPaste,
|
||||||
detectScroll = true,
|
detectScroll = true,
|
||||||
|
handleKeyboardGlobally = false,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const canvasActions = props.UIOptions?.canvasActions;
|
const canvasActions = props.UIOptions?.canvasActions;
|
||||||
@ -82,6 +83,7 @@ const Excalidraw = (props: ExcalidrawProps) => {
|
|||||||
UIOptions={UIOptions}
|
UIOptions={UIOptions}
|
||||||
onPaste={onPaste}
|
onPaste={onPaste}
|
||||||
detectScroll={detectScroll}
|
detectScroll={detectScroll}
|
||||||
|
handleKeyboardGlobally={handleKeyboardGlobally}
|
||||||
/>
|
/>
|
||||||
</InitializeApp>
|
</InitializeApp>
|
||||||
);
|
);
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// 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 {
|
Object {
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"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 {
|
Object {
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"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 {
|
Object {
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"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 {
|
Object {
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"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 {
|
Object {
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
|
@ -24,7 +24,8 @@ beforeEach(() => {
|
|||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
describe("add element to the scene when pointer dragging long enough", () => {
|
describe("Test dragCreate", () => {
|
||||||
|
describe("add element to the scene when pointer dragging long enough", () => {
|
||||||
it("rectangle", async () => {
|
it("rectangle", async () => {
|
||||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||||
// select tool
|
// select tool
|
||||||
@ -186,9 +187,9 @@ describe("add element to the scene when pointer dragging long enough", () => {
|
|||||||
|
|
||||||
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("do not add element to the scene if size is too small", () => {
|
describe("do not add element to the scene if size is too small", () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
mockBoundingClientRect();
|
mockBoundingClientRect();
|
||||||
});
|
});
|
||||||
@ -268,7 +269,9 @@ describe("do not add element to the scene if size is too small", () => {
|
|||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
// we need to finalize it because arrows and lines enter multi-mode
|
// we need to finalize it because arrows and lines enter multi-mode
|
||||||
fireEvent.keyDown(document, { key: KEYS.ENTER });
|
fireEvent.keyDown(document, {
|
||||||
|
key: KEYS.ENTER,
|
||||||
|
});
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
expect(renderScene).toHaveBeenCalledTimes(7);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
@ -290,10 +293,13 @@ describe("do not add element to the scene if size is too small", () => {
|
|||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
// we need to finalize it because arrows and lines enter multi-mode
|
// we need to finalize it because arrows and lines enter multi-mode
|
||||||
fireEvent.keyDown(document, { key: KEYS.ENTER });
|
fireEvent.keyDown(document, {
|
||||||
|
key: KEYS.ENTER,
|
||||||
|
});
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
expect(renderScene).toHaveBeenCalledTimes(7);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -99,7 +99,9 @@ describe("multi point mode in linear elements", () => {
|
|||||||
// done
|
// done
|
||||||
fireEvent.pointerDown(canvas);
|
fireEvent.pointerDown(canvas);
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
fireEvent.keyDown(document, { key: KEYS.ENTER });
|
fireEvent.keyDown(document, {
|
||||||
|
key: KEYS.ENTER,
|
||||||
|
});
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(14);
|
expect(renderScene).toHaveBeenCalledTimes(14);
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
@ -140,7 +142,9 @@ describe("multi point mode in linear elements", () => {
|
|||||||
// done
|
// done
|
||||||
fireEvent.pointerDown(canvas);
|
fireEvent.pointerDown(canvas);
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
fireEvent.keyDown(document, { key: KEYS.ENTER });
|
fireEvent.keyDown(document, {
|
||||||
|
key: KEYS.ENTER,
|
||||||
|
});
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(14);
|
expect(renderScene).toHaveBeenCalledTimes(14);
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
|
@ -413,11 +413,23 @@ describe("regression tests", () => {
|
|||||||
|
|
||||||
it("zoom hotkeys", () => {
|
it("zoom hotkeys", () => {
|
||||||
expect(h.state.zoom.value).toBe(1);
|
expect(h.state.zoom.value).toBe(1);
|
||||||
fireEvent.keyDown(document, { code: CODES.EQUAL, ctrlKey: true });
|
fireEvent.keyDown(document, {
|
||||||
fireEvent.keyUp(document, { code: CODES.EQUAL, ctrlKey: true });
|
code: CODES.EQUAL,
|
||||||
|
ctrlKey: true,
|
||||||
|
});
|
||||||
|
fireEvent.keyUp(document, {
|
||||||
|
code: CODES.EQUAL,
|
||||||
|
ctrlKey: true,
|
||||||
|
});
|
||||||
expect(h.state.zoom.value).toBeGreaterThan(1);
|
expect(h.state.zoom.value).toBeGreaterThan(1);
|
||||||
fireEvent.keyDown(document, { code: CODES.MINUS, ctrlKey: true });
|
fireEvent.keyDown(document, {
|
||||||
fireEvent.keyUp(document, { code: CODES.MINUS, ctrlKey: true });
|
code: CODES.MINUS,
|
||||||
|
ctrlKey: true,
|
||||||
|
});
|
||||||
|
fireEvent.keyUp(document, {
|
||||||
|
code: CODES.MINUS,
|
||||||
|
ctrlKey: true,
|
||||||
|
});
|
||||||
expect(h.state.zoom.value).toBe(1);
|
expect(h.state.zoom.value).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -100,7 +100,9 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
fireEvent.keyDown(document, { key: KEYS.ESCAPE });
|
fireEvent.keyDown(document, {
|
||||||
|
key: KEYS.ESCAPE,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const tool = getByToolName("selection");
|
const tool = getByToolName("selection");
|
||||||
@ -127,7 +129,9 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
fireEvent.keyDown(document, { key: KEYS.ESCAPE });
|
fireEvent.keyDown(document, {
|
||||||
|
key: KEYS.ESCAPE,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const tool = getByToolName("selection");
|
const tool = getByToolName("selection");
|
||||||
@ -154,7 +158,9 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
fireEvent.keyDown(document, { key: KEYS.ESCAPE });
|
fireEvent.keyDown(document, {
|
||||||
|
key: KEYS.ESCAPE,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const tool = getByToolName("selection");
|
const tool = getByToolName("selection");
|
||||||
@ -181,7 +187,9 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||||
fireEvent.pointerUp(canvas);
|
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.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
fireEvent.keyDown(document, { key: KEYS.ESCAPE });
|
fireEvent.keyDown(document, {
|
||||||
|
key: KEYS.ESCAPE,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -196,6 +196,7 @@ export interface ExcalidrawProps {
|
|||||||
) => JSX.Element;
|
) => JSX.Element;
|
||||||
UIOptions?: UIOptions;
|
UIOptions?: UIOptions;
|
||||||
detectScroll?: boolean;
|
detectScroll?: boolean;
|
||||||
|
handleKeyboardGlobally?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SceneData = {
|
export type SceneData = {
|
||||||
@ -230,4 +231,5 @@ export type AppProps = ExcalidrawProps & {
|
|||||||
canvasActions: Required<CanvasActions>;
|
canvasActions: Required<CanvasActions>;
|
||||||
};
|
};
|
||||||
detectScroll: boolean;
|
detectScroll: boolean;
|
||||||
|
handleKeyboardGlobally: boolean;
|
||||||
};
|
};
|
||||||
|
11
src/utils.ts
11
src/utils.ts
@ -427,3 +427,14 @@ export const getNearestScrollableContainer = (
|
|||||||
}
|
}
|
||||||
return document;
|
return document;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const focusNearestParent = (element: HTMLInputElement) => {
|
||||||
|
let parent = element.parentElement;
|
||||||
|
while (parent) {
|
||||||
|
if (parent.tabIndex > -1) {
|
||||||
|
parent.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parent = parent.parentElement;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user