diff --git a/public/index.html b/public/index.html index 0e7353a26..c15ac97fa 100644 --- a/public/index.html +++ b/public/index.html @@ -112,8 +112,7 @@ Roboto, Helvetica, Arial, sans-serif; font-family: var(--ui-font); -webkit-text-size-adjust: 100%; - -webkit-user-select: none; - user-select: none; + width: 100vw; height: 100vh; } diff --git a/src/components/App.tsx b/src/components/App.tsx index 392e7059c..215304783 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1105,7 +1105,6 @@ class App extends React.Component { }; private onTapEnd = (event: TouchEvent) => { - event.preventDefault(); if (event.touches.length > 0) { this.setState({ previousSelectedElementIds: {}, @@ -1631,10 +1630,12 @@ class App extends React.Component { updateBoundElements(element); } }), - onSubmit: withBatchedUpdates((text) => { + onSubmit: withBatchedUpdates(({ text, viaKeyboard }) => { const isDeleted = !text.trim(); updateElement(text, isDeleted); - if (!isDeleted) { + // select the created text element only if submitting via keyboard + // (when submitting via click it should act as signal to deselect) + if (!isDeleted && viaKeyboard) { this.setState((prevState) => ({ selectedElementIds: { ...prevState.selectedElementIds, @@ -2151,15 +2152,6 @@ class App extends React.Component { this.updateGestureOnPointerDown(event); - // fixes pointermove causing selection of UI texts #32 - event.preventDefault(); - // Preventing the event above disables default behavior - // of defocusing potentially focused element, which is what we - // want when clicking inside the canvas. - if (document.activeElement instanceof HTMLElement) { - document.activeElement.blur(); - } - // don't select while panning if (gesture.pointers.size > 1) { return; diff --git a/src/css/styles.scss b/src/css/styles.scss index dbe9ae827..863b11ccf 100644 --- a/src/css/styles.scss +++ b/src/css/styles.scss @@ -17,6 +17,13 @@ left: 0; right: 0; + // serves 2 purposes: + // 1. prevent selecting text outside the component when double-clicking or + // dragging inside it (e.g. on canvas) + // 2. prevent selecting UI, both from the inside, and from outside the + // component (e.g. if you select text in a sidebar) + user-select: none; + a { font-weight: 500; text-decoration: none; @@ -29,7 +36,6 @@ canvas { touch-action: none; - user-select: none; // following props improve blurriness at certain devicePixelRatios. // AFAIK it doesn't affect export (in fact, export seems sharp either way). diff --git a/src/element/textWysiwyg.tsx b/src/element/textWysiwyg.tsx index 5f0ca55a9..93ef78d04 100644 --- a/src/element/textWysiwyg.tsx +++ b/src/element/textWysiwyg.tsx @@ -47,7 +47,7 @@ export const textWysiwyg = ({ id: ExcalidrawElement["id"]; appState: AppState; onChange?: (text: string) => void; - onSubmit: (text: string) => void; + onSubmit: (data: { text: string; viaKeyboard: boolean }) => void; getViewportCoords: (x: number, y: number) => [number, number]; element: ExcalidrawElement; canvas: HTMLCanvasElement | null; @@ -136,12 +136,14 @@ export const textWysiwyg = ({ editable.onkeydown = (event) => { if (event.key === KEYS.ESCAPE) { event.preventDefault(); + submittedViaKeyboard = true; handleSubmit(); } else if (event.key === KEYS.ENTER && event[KEYS.CTRL_OR_CMD]) { event.preventDefault(); if (event.isComposing || event.keyCode === 229) { return; } + submittedViaKeyboard = true; handleSubmit(); } else if (event.key === KEYS.ENTER && !event.altKey) { event.stopPropagation(); @@ -153,8 +155,14 @@ export const textWysiwyg = ({ event.stopPropagation(); }; + // using a state variable instead of passing it to the handleSubmit callback + // so that we don't need to create separate a callback for event handlers + let submittedViaKeyboard = false; const handleSubmit = () => { - onSubmit(normalizeText(editable.value)); + onSubmit({ + text: normalizeText(editable.value), + viaKeyboard: submittedViaKeyboard, + }); cleanup(); }; @@ -175,7 +183,7 @@ export const textWysiwyg = ({ window.removeEventListener("resize", updateWysiwygStyle); window.removeEventListener("wheel", stopEvent, true); window.removeEventListener("pointerdown", onPointerDown); - window.removeEventListener("pointerup", rebindBlur); + window.removeEventListener("pointerup", bindBlurEvent); window.removeEventListener("blur", handleSubmit); unbindUpdate(); @@ -183,10 +191,12 @@ export const textWysiwyg = ({ editable.remove(); }; - const rebindBlur = () => { - window.removeEventListener("pointerup", rebindBlur); - // deferred to guard against focus traps on various UIs that steal focus - // upon pointerUp + const bindBlurEvent = () => { + window.removeEventListener("pointerup", bindBlurEvent); + // Deferred so that the pointerdown that initiates the wysiwyg doesn't + // trigger the blur on ensuing pointerup. + // Also to handle cases such as picking a color which would trigger a blur + // in that same tick. setTimeout(() => { editable.onblur = handleSubmit; // case: clicking on the same property → no change → no update → no focus @@ -202,7 +212,7 @@ export const textWysiwyg = ({ !isWritableElement(event.target) ) { editable.onblur = null; - window.addEventListener("pointerup", rebindBlur); + window.addEventListener("pointerup", bindBlurEvent); // handle edge-case where pointerup doesn't fire e.g. due to user // alt-tabbing away window.addEventListener("blur", handleSubmit); @@ -215,9 +225,14 @@ export const textWysiwyg = ({ editable.focus(); }); + // --------------------------------------------------------------------------- + let isDestroyed = false; - editable.onblur = handleSubmit; + // select on init (focusing is done separately inside the bindBlurEvent() + // because we need it to happen *after* the blur event from `pointerdown`) + editable.select(); + bindBlurEvent(); // reposition wysiwyg in case of canvas is resized. Using ResizeObserver // is preferred so we catch changes from host, where window may not resize. @@ -239,6 +254,4 @@ export const textWysiwyg = ({ document .querySelector(".excalidraw-textEditorContainer")! .appendChild(editable); - editable.focus(); - editable.select(); };