Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/editor/src/lib/editor/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11341,7 +11341,10 @@ export class Editor extends EventEmitter<TLEventMap> {
// Don't pass right-click panning events to the state chart
// as it causes unintended shape selection on release
if (slideSpeed > 0) {
this.slideCamera({ speed: slideSpeed, direction: slideDirection })
this.slideCamera({
speed: slideSpeed,
direction: { x: slideDirection.x, y: slideDirection.y, z: 0 },
})
}
this._selectedShapeIdsAtPointerDown = []
this._didCaptureSelectionAtPointerDown = false
Expand Down
20 changes: 15 additions & 5 deletions packages/tldraw/src/lib/ui/hooks/useKeyboardShortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,21 @@ export function useKeyboardShortcuts() {
if (!reg.onKeyDown) continue
for (const p of reg.parsed) {
if (matchesEvent(e, p)) {
const prev = code ? heldKeyRegistrations.get(code) : undefined
// The held key already triggered a different shortcut; don't fall back to
// this one (or anything else) just because a modifier was released.
if (prev && prev !== reg) return
if (code) heldKeyRegistrations.set(code, reg)
if (code) {
// We only guard auto-repeat events. A fresh keypress (`e.repeat` is
// false) is always free to trigger whatever it matches, even on the same
// physical key — e.g. cmd+z (undo) then cmd+shift+z (redo), where macOS
// swallows the `z` keyup while cmd stays held, so we can't rely on keyup
// to clear the previous registration.
if (e.repeat) {
const prev = heldKeyRegistrations.get(code)
// The held key already triggered a different shortcut; don't fall back
// to this one (or anything else) just because a modifier was released.
if (prev && prev !== reg) return
} else {
heldKeyRegistrations.set(code, reg)
}
}
reg.onKeyDown(e)
break
}
Expand Down
11 changes: 11 additions & 0 deletions packages/tldraw/src/test/rightClickPanning.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Vec } from '@tldraw/editor'
import { TestEditor } from './TestEditor'

describe('with rightClickPanning enabled (default)', () => {
Expand All @@ -17,6 +18,16 @@ describe('with rightClickPanning enabled (default)', () => {
expect(editor.inputs.getIsPanning()).toBe(false)
})

it('does not zoom when momentum panning on release', () => {
editor.pointerDown(100, 100, { button: 2 })
editor.pointerMove(200, 200)
expect(editor.inputs.getIsPanning()).toBe(true)
editor.inputs.setPointerVelocity(new Vec(1, 1))
editor.pointerUp(200, 200, { button: 2 }).forceTick()

expect(editor.getCamera().z).toBe(1)
})

it('does not pan on a static right-click', () => {
editor.pointerDown(100, 100, { button: 2 })
editor.pointerUp(100, 100, { button: 2 })
Expand Down
27 changes: 26 additions & 1 deletion packages/tldraw/src/test/ui/keyboardShortcuts.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { act } from '@testing-library/react'
import { Editor } from '@tldraw/editor'
import { createShapeId, Editor } from '@tldraw/editor'
import { useEffect } from 'react'
import { Tldraw } from '../../lib/Tldraw'
import { useActions } from '../../lib/ui/context/actions'
Expand Down Expand Up @@ -179,4 +179,29 @@ describe('keyboard shortcuts with a held key', () => {
keydown(editor, { key: 'q', code: 'KeyQ' })
expect(editor.getInstanceState().isToolLocked).toBe(true)
})

// Regression test for #9099: redo (cmd+shift+z) stopped firing after an undo (cmd+z) on
// macOS, where the browser swallows the `z` keyup while cmd stays held. The held-key
// tracking from #9099 never got cleared, so the stale undo registration blocked the redo
// on the same physical `KeyZ`. A fresh keypress must always be free to trigger its match.
it('fires redo after undo on the same physical key when the keyup is swallowed (cmd held)', async () => {
const { editor } = await setupFocusedEditor()

const id = createShapeId()
act(() => {
editor.markHistoryStoppingPoint()
editor.createShape({ id, type: 'geo', x: 0, y: 0 })
})
expect(editor.getCurrentPageShapeIds().has(id)).toBe(true)

// cmd+z undoes the shape creation. On macOS the `z` keyup is never delivered while cmd
// stays held, so we deliberately don't dispatch it.
keydown(editor, { key: 'z', code: 'KeyZ', metaKey: true })
expect(editor.getCurrentPageShapeIds().has(id)).toBe(false)

// Adding shift and pressing z again is a fresh keypress (not an auto-repeat), so it must
// trigger redo rather than being blocked by the stale undo registration on `KeyZ`.
keydown(editor, { key: 'z', code: 'KeyZ', metaKey: true, shiftKey: true })
expect(editor.getCurrentPageShapeIds().has(id)).toBe(true)
})
})
Loading