Let the native clipboard through where the canvas has nothing to copy

A bound chord used to be `preventDefault`ed before its handler ran, so the
canvas took every ⌘C and ⌘V whether or not the node clipboard would act —
which is what blocked copying selected text out of the chrome floating over
the canvas. A binding may now return `false` to decline the key; ⌘C does so
when there is a text selection or no node selected, ⌘V when nothing of ours
is stored.
This commit is contained in:
2026-09-06 18:16:17 +02:00
parent 7d8fe6456a
commit 23a80db6bb
3 changed files with 115 additions and 4 deletions
+15 -2
View File
@@ -1061,8 +1061,21 @@ function FlowEditorInner({
{
"mod+z": () => step(true),
"mod+shift+z": () => step(false),
"mod+c": () => void copyNodes(),
"mod+v": pasteNodes,
// Both decline the key unless the node clipboard has something to do
// with it, so the native copy and paste still work on the canvas. A
// text selection wins outright: that is what the user asked to copy.
"mod+c": () => {
const selection = document.getSelection()
if (selection && !selection.isCollapsed) return false
if (!canvasNodes.some((node) => node.selected) && !selectedId) {
return false
}
void copyNodes()
},
"mod+v": () => {
if (!safeStorage.get(CLIPBOARD_KEY)) return false
pasteNodes()
},
// ⌘P, not ⌘K: the sidebar's global search owns that everywhere, and this
// palette is the canvas's own, narrower thing.
"mod+p": () => setPaletteOpen((open) => !open),
+6 -2
View File
@@ -6,8 +6,12 @@ import { useEffect, useRef } from "react"
* A chord reads as `mod+shift+z`, where `mod` is ⌘ on a Mac and Ctrl
* everywhere else. Bindings are plain data, so a view declares the whole set
* it answers to in one place.
*
* A binding that returns `false` declines the key: it did nothing, so the
* browser's own default runs. That is how ⌘C stays a native copy whenever the
* canvas has nothing of its own to copy.
*/
export type Shortcuts = Record<string, (event: KeyboardEvent) => void>
export type Shortcuts = Record<string, (event: KeyboardEvent) => unknown>
/** Text fields and the code editor keep their own bindings and their own undo. */
export function isTextEntry(target: EventTarget | null): boolean {
@@ -55,8 +59,8 @@ export function useShortcuts(bindings: Shortcuts, inTextEntry: string[] = []) {
) {
return
}
if (run(event) === false) return
event.preventDefault()
run(event)
}
window.addEventListener("keydown", onKeyDown)
return () => window.removeEventListener("keydown", onKeyDown)