From fc1dde724e925a2be4bcbafa263b316bbab717b9 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Thu, 23 Sep 2021 12:14:22 +0100 Subject: [PATCH] Makes keyboard events work again (#114) --- .../core/src/components/canvas/canvas.tsx | 3 ++ packages/core/src/hooks/index.ts | 1 + packages/core/src/hooks/useKeyEvents.ts | 25 ++++++++++ packages/core/src/types.ts | 6 +++ .../tldraw/src/components/tldraw/tldraw.tsx | 2 + .../tldraw/src/hooks/useKeyboardShortcuts.tsx | 18 ------- .../sessions/rotate/rotate.session.spec.ts | 47 +++++++++++++++++++ .../session/sessions/rotate/rotate.session.ts | 25 +++++++--- packages/tldraw/src/state/tlstate.ts | 20 +++----- 9 files changed, 108 insertions(+), 39 deletions(-) create mode 100644 packages/core/src/hooks/useKeyEvents.ts diff --git a/packages/core/src/components/canvas/canvas.tsx b/packages/core/src/components/canvas/canvas.tsx index 06a2bbd93..abb40f155 100644 --- a/packages/core/src/components/canvas/canvas.tsx +++ b/packages/core/src/components/canvas/canvas.tsx @@ -6,6 +6,7 @@ import { useSafariFocusOutFix, useCanvasEvents, useCameraCss, + useKeyEvents, } from '+hooks' import type { TLBinding, TLPage, TLPageState, TLShape } from '+types' import { ErrorFallback } from '+components/error-fallback' @@ -57,6 +58,8 @@ export function Canvas>({ e.currentTarget.scrollTo(0, 0) }, []) + useKeyEvents() + return (
{ + const handleKeyDown = (e: KeyboardEvent) => { + callbacks.onKeyDown?.(e.key, inputs.keydown(e), e) + } + + const handleKeyUp = (e: KeyboardEvent) => { + inputs.keyup(e) + callbacks.onKeyUp?.(e.key, inputs.keyup(e), e) + } + + window.addEventListener('keydown', handleKeyDown) + window.addEventListener('keyup', handleKeyUp) + + return () => { + window.removeEventListener('keydown', handleKeyDown) + window.removeEventListener('keyup', handleKeyUp) + } + }, [inputs, callbacks]) +} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 2d00170b0..7d1053035 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -133,6 +133,8 @@ export type TLShapeChangeHandler = ( export type TLShapeBlurHandler = (info?: K) => void +export type TLKeyboardEventHandler = (key: string, info: TLKeyboardInfo, e: KeyboardEvent) => void + export type TLPointerEventHandler = (info: TLPointerInfo, e: React.PointerEvent) => void export type TLCanvasEventHandler = (info: TLPointerInfo<'canvas'>, e: React.PointerEvent) => void @@ -206,6 +208,10 @@ export interface TLCallbacks { onRenderCountChange: (ids: string[]) => void onError: (error: Error) => void onBoundsChange: (bounds: TLBounds) => void + + // Keyboard event handlers + onKeyDown: TLKeyboardEventHandler + onKeyUp: TLKeyboardEventHandler } export interface TLBounds { diff --git a/packages/tldraw/src/components/tldraw/tldraw.tsx b/packages/tldraw/src/components/tldraw/tldraw.tsx index 17b8f8f08..24402bf9a 100644 --- a/packages/tldraw/src/components/tldraw/tldraw.tsx +++ b/packages/tldraw/src/components/tldraw/tldraw.tsx @@ -240,6 +240,8 @@ function InnerTldraw({ onShapeChange={tlstate.onShapeChange} onShapeBlur={tlstate.onShapeBlur} onBoundsChange={tlstate.updateBounds} + onKeyDown={tlstate.onKeyDown} + onKeyUp={tlstate.onKeyUp} /> {isFocusMode ? ( diff --git a/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx b/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx index 3a8c50287..8091bf17d 100644 --- a/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx +++ b/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx @@ -11,24 +11,6 @@ export function useKeyboardShortcuts(ref: React.RefObject) { return elm && (document.activeElement === elm || elm.contains(document.activeElement)) }, [ref]) - React.useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (canHandleEvent()) tlstate.onKeyDown(e.key) - } - - const handleKeyUp = (e: KeyboardEvent) => { - if (canHandleEvent()) tlstate.onKeyUp(e.key) - } - - window.addEventListener('keydown', handleKeyDown) - window.addEventListener('keyup', handleKeyUp) - - return () => { - window.removeEventListener('keydown', handleKeyDown) - window.removeEventListener('keyup', handleKeyUp) - } - }, [tlstate]) - /* ---------------------- Tools --------------------- */ useHotkeys( diff --git a/packages/tldraw/src/state/session/sessions/rotate/rotate.session.spec.ts b/packages/tldraw/src/state/session/sessions/rotate/rotate.session.spec.ts index 86c11c844..acd6a09a9 100644 --- a/packages/tldraw/src/state/session/sessions/rotate/rotate.session.spec.ts +++ b/packages/tldraw/src/state/session/sessions/rotate/rotate.session.spec.ts @@ -61,6 +61,53 @@ describe('Rotate session', () => { it.todo('rotates handles only on shapes with handles') + describe('when rotating a single shape while pressing shift', () => { + it('Clamps rotation to 15 degrees', () => { + const tlstate = new TLDrawState() + .loadDocument(mockDocument) + .select('rect1') + .startTransformSession([0, 0], 'rotate') + .updateTransformSession([20, 10], true) + .completeSession() + + expect(Math.round((tlstate.getShape('rect1').rotation || 0) * (180 / Math.PI)) % 15).toEqual( + 0 + ) + }) + + it('Clamps rotation to 15 degrees when starting from a rotation', () => { + // Rect 1 is a little rotated + const tlstate = new TLDrawState() + .loadDocument(mockDocument) + .select('rect1') + .startTransformSession([0, 0], 'rotate') + .updateTransformSession([5, 5]) + .completeSession() + + // Rect 1 clamp rotated, starting from a little rotation + tlstate + .select('rect1') + .startTransformSession([0, 0], 'rotate') + .updateTransformSession([100, 200], true) + .completeSession() + + expect(Math.round((tlstate.getShape('rect1').rotation || 0) * (180 / Math.PI)) % 15).toEqual( + 0 + ) + + // Try again, too. + tlstate + .select('rect1') + .startTransformSession([0, 0], 'rotate') + .updateTransformSession([-100, 5000], true) + .completeSession() + + expect(Math.round((tlstate.getShape('rect1').rotation || 0) * (180 / Math.PI)) % 15).toEqual( + 0 + ) + }) + }) + describe('when rotating multiple shapes', () => { it('keeps the center', () => { tlstate.loadDocument(mockDocument).select('rect1', 'rect2') diff --git a/packages/tldraw/src/state/session/sessions/rotate/rotate.session.ts b/packages/tldraw/src/state/session/sessions/rotate/rotate.session.ts index 81e604d27..6b92a0609 100644 --- a/packages/tldraw/src/state/session/sessions/rotate/rotate.session.ts +++ b/packages/tldraw/src/state/session/sessions/rotate/rotate.session.ts @@ -30,19 +30,28 @@ export class RotateSession implements Session { const shapes: Record> = {} - const nextDirection = Vec.angle(commonBoundsCenter, point) - this.initialAngle - - let nextBoundsRotation = this.snapshot.boundsRotation + nextDirection + let directionDelta = Vec.angle(commonBoundsCenter, point) - this.initialAngle if (isLocked) { - nextBoundsRotation = Utils.snapAngleToSegments(nextBoundsRotation, 24) + directionDelta = Utils.snapAngleToSegments(directionDelta, 24) // 15 degrees } - const delta = nextBoundsRotation - this.snapshot.boundsRotation - // Update the shapes initialShapes.forEach(({ id, center, shape }) => { - const change = TLDR.getRotatedShapeMutation(shape, center, commonBoundsCenter, delta) + const { rotation = 0 } = shape + let shapeDelta = 0 + + if (isLocked) { + const snappedRotation = Utils.snapAngleToSegments(rotation, 24) + shapeDelta = snappedRotation - rotation + } + + const change = TLDR.getRotatedShapeMutation( + shape, + center, + commonBoundsCenter, + isLocked ? directionDelta + shapeDelta : directionDelta + ) if (change) { shapes[id] = change @@ -51,6 +60,8 @@ export class RotateSession implements Session { this.changes = shapes + const nextBoundsRotation = this.snapshot.boundsRotation + directionDelta + return { document: { pages: { diff --git a/packages/tldraw/src/state/tlstate.ts b/packages/tldraw/src/state/tlstate.ts index 11cd7a2b2..7b96ea90d 100644 --- a/packages/tldraw/src/state/tlstate.ts +++ b/packages/tldraw/src/state/tlstate.ts @@ -5,6 +5,7 @@ import { TLBoundsEdge, TLBoundsEventHandler, TLBoundsHandleEventHandler, + TLKeyboardEventHandler, TLCanvasEventHandler, TLPageState, TLPinchEventHandler, @@ -361,10 +362,6 @@ export class TLDrawState extends StateManager { ) } - handleMount = (inputs: Inputs): void => { - this.inputs = inputs - } - /** * Update the bounding box when the renderer's bounds change. * @param bounds @@ -2261,16 +2258,14 @@ export class TLDrawState extends StateManager { /* ----------------- Keyboard Events ---------------- */ - onKeyDown = (key: string) => { - const info = this.inputs?.pointer - if (!info) return - + onKeyDown: TLKeyboardEventHandler = (key, info) => { if (key === 'Escape') { this.cancel() - return } + if (!info) return + switch (this.appState.status.current) { case TLDrawStatus.Idle: { break @@ -2296,9 +2291,7 @@ export class TLDrawState extends StateManager { case TLDrawStatus.Transforming: { if (key === 'Escape') { this.cancelSession(this.getPagePoint(info.point)) - } - - if (key === 'Shift' || key === 'Alt') { + } else if (key === 'Shift' || key === 'Alt') { this.updateTransformSession(this.getPagePoint(info.point), info.shiftKey, info.altKey) } break @@ -2321,8 +2314,7 @@ export class TLDrawState extends StateManager { } } - onKeyUp = (key: string) => { - const info = this.inputs?.pointer + onKeyUp: TLKeyboardEventHandler = (key, info) => { if (!info) return switch (this.appState.status.current) {