Makes keyboard events work again (#114)
This commit is contained in:
parent
48f784c322
commit
fc1dde724e
9 changed files with 108 additions and 39 deletions
|
@ -6,6 +6,7 @@ import {
|
||||||
useSafariFocusOutFix,
|
useSafariFocusOutFix,
|
||||||
useCanvasEvents,
|
useCanvasEvents,
|
||||||
useCameraCss,
|
useCameraCss,
|
||||||
|
useKeyEvents,
|
||||||
} from '+hooks'
|
} from '+hooks'
|
||||||
import type { TLBinding, TLPage, TLPageState, TLShape } from '+types'
|
import type { TLBinding, TLPage, TLPageState, TLShape } from '+types'
|
||||||
import { ErrorFallback } from '+components/error-fallback'
|
import { ErrorFallback } from '+components/error-fallback'
|
||||||
|
@ -57,6 +58,8 @@ export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
|
||||||
e.currentTarget.scrollTo(0, 0)
|
e.currentTarget.scrollTo(0, 0)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useKeyEvents()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={id} className="tl-container" ref={rContainer}>
|
<div id={id} className="tl-container" ref={rContainer}>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -14,3 +14,4 @@ export * from './useHandles'
|
||||||
export * from './usePreventNavigation'
|
export * from './usePreventNavigation'
|
||||||
export * from './useBoundsEvents'
|
export * from './useBoundsEvents'
|
||||||
export * from './usePosition'
|
export * from './usePosition'
|
||||||
|
export * from './useKeyEvents'
|
||||||
|
|
25
packages/core/src/hooks/useKeyEvents.ts
Normal file
25
packages/core/src/hooks/useKeyEvents.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { useTLContext } from '+hooks'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
export function useKeyEvents() {
|
||||||
|
const { inputs, callbacks } = useTLContext()
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
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])
|
||||||
|
}
|
|
@ -133,6 +133,8 @@ export type TLShapeChangeHandler<T, K = any> = (
|
||||||
|
|
||||||
export type TLShapeBlurHandler<K = any> = (info?: K) => void
|
export type TLShapeBlurHandler<K = any> = (info?: K) => void
|
||||||
|
|
||||||
|
export type TLKeyboardEventHandler = (key: string, info: TLKeyboardInfo, e: KeyboardEvent) => void
|
||||||
|
|
||||||
export type TLPointerEventHandler = (info: TLPointerInfo<string>, e: React.PointerEvent) => void
|
export type TLPointerEventHandler = (info: TLPointerInfo<string>, e: React.PointerEvent) => void
|
||||||
|
|
||||||
export type TLCanvasEventHandler = (info: TLPointerInfo<'canvas'>, e: React.PointerEvent) => void
|
export type TLCanvasEventHandler = (info: TLPointerInfo<'canvas'>, e: React.PointerEvent) => void
|
||||||
|
@ -206,6 +208,10 @@ export interface TLCallbacks<T extends TLShape> {
|
||||||
onRenderCountChange: (ids: string[]) => void
|
onRenderCountChange: (ids: string[]) => void
|
||||||
onError: (error: Error) => void
|
onError: (error: Error) => void
|
||||||
onBoundsChange: (bounds: TLBounds) => void
|
onBoundsChange: (bounds: TLBounds) => void
|
||||||
|
|
||||||
|
// Keyboard event handlers
|
||||||
|
onKeyDown: TLKeyboardEventHandler
|
||||||
|
onKeyUp: TLKeyboardEventHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TLBounds {
|
export interface TLBounds {
|
||||||
|
|
|
@ -240,6 +240,8 @@ function InnerTldraw({
|
||||||
onShapeChange={tlstate.onShapeChange}
|
onShapeChange={tlstate.onShapeChange}
|
||||||
onShapeBlur={tlstate.onShapeBlur}
|
onShapeBlur={tlstate.onShapeBlur}
|
||||||
onBoundsChange={tlstate.updateBounds}
|
onBoundsChange={tlstate.updateBounds}
|
||||||
|
onKeyDown={tlstate.onKeyDown}
|
||||||
|
onKeyUp={tlstate.onKeyUp}
|
||||||
/>
|
/>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
{isFocusMode ? (
|
{isFocusMode ? (
|
||||||
|
|
|
@ -11,24 +11,6 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
return elm && (document.activeElement === elm || elm.contains(document.activeElement))
|
return elm && (document.activeElement === elm || elm.contains(document.activeElement))
|
||||||
}, [ref])
|
}, [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 --------------------- */
|
/* ---------------------- Tools --------------------- */
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
|
|
|
@ -61,6 +61,53 @@ describe('Rotate session', () => {
|
||||||
|
|
||||||
it.todo('rotates handles only on shapes with handles')
|
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', () => {
|
describe('when rotating multiple shapes', () => {
|
||||||
it('keeps the center', () => {
|
it('keeps the center', () => {
|
||||||
tlstate.loadDocument(mockDocument).select('rect1', 'rect2')
|
tlstate.loadDocument(mockDocument).select('rect1', 'rect2')
|
||||||
|
|
|
@ -30,19 +30,28 @@ export class RotateSession implements Session {
|
||||||
|
|
||||||
const shapes: Record<string, Partial<TLDrawShape>> = {}
|
const shapes: Record<string, Partial<TLDrawShape>> = {}
|
||||||
|
|
||||||
const nextDirection = Vec.angle(commonBoundsCenter, point) - this.initialAngle
|
let directionDelta = Vec.angle(commonBoundsCenter, point) - this.initialAngle
|
||||||
|
|
||||||
let nextBoundsRotation = this.snapshot.boundsRotation + nextDirection
|
|
||||||
|
|
||||||
if (isLocked) {
|
if (isLocked) {
|
||||||
nextBoundsRotation = Utils.snapAngleToSegments(nextBoundsRotation, 24)
|
directionDelta = Utils.snapAngleToSegments(directionDelta, 24) // 15 degrees
|
||||||
}
|
}
|
||||||
|
|
||||||
const delta = nextBoundsRotation - this.snapshot.boundsRotation
|
|
||||||
|
|
||||||
// Update the shapes
|
// Update the shapes
|
||||||
initialShapes.forEach(({ id, center, shape }) => {
|
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) {
|
if (change) {
|
||||||
shapes[id] = change
|
shapes[id] = change
|
||||||
|
@ -51,6 +60,8 @@ export class RotateSession implements Session {
|
||||||
|
|
||||||
this.changes = shapes
|
this.changes = shapes
|
||||||
|
|
||||||
|
const nextBoundsRotation = this.snapshot.boundsRotation + directionDelta
|
||||||
|
|
||||||
return {
|
return {
|
||||||
document: {
|
document: {
|
||||||
pages: {
|
pages: {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
TLBoundsEdge,
|
TLBoundsEdge,
|
||||||
TLBoundsEventHandler,
|
TLBoundsEventHandler,
|
||||||
TLBoundsHandleEventHandler,
|
TLBoundsHandleEventHandler,
|
||||||
|
TLKeyboardEventHandler,
|
||||||
TLCanvasEventHandler,
|
TLCanvasEventHandler,
|
||||||
TLPageState,
|
TLPageState,
|
||||||
TLPinchEventHandler,
|
TLPinchEventHandler,
|
||||||
|
@ -361,10 +362,6 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMount = (inputs: Inputs): void => {
|
|
||||||
this.inputs = inputs
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the bounding box when the renderer's bounds change.
|
* Update the bounding box when the renderer's bounds change.
|
||||||
* @param bounds
|
* @param bounds
|
||||||
|
@ -2261,16 +2258,14 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
|
|
||||||
/* ----------------- Keyboard Events ---------------- */
|
/* ----------------- Keyboard Events ---------------- */
|
||||||
|
|
||||||
onKeyDown = (key: string) => {
|
onKeyDown: TLKeyboardEventHandler = (key, info) => {
|
||||||
const info = this.inputs?.pointer
|
|
||||||
if (!info) return
|
|
||||||
|
|
||||||
if (key === 'Escape') {
|
if (key === 'Escape') {
|
||||||
this.cancel()
|
this.cancel()
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!info) return
|
||||||
|
|
||||||
switch (this.appState.status.current) {
|
switch (this.appState.status.current) {
|
||||||
case TLDrawStatus.Idle: {
|
case TLDrawStatus.Idle: {
|
||||||
break
|
break
|
||||||
|
@ -2296,9 +2291,7 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
case TLDrawStatus.Transforming: {
|
case TLDrawStatus.Transforming: {
|
||||||
if (key === 'Escape') {
|
if (key === 'Escape') {
|
||||||
this.cancelSession(this.getPagePoint(info.point))
|
this.cancelSession(this.getPagePoint(info.point))
|
||||||
}
|
} else if (key === 'Shift' || key === 'Alt') {
|
||||||
|
|
||||||
if (key === 'Shift' || key === 'Alt') {
|
|
||||||
this.updateTransformSession(this.getPagePoint(info.point), info.shiftKey, info.altKey)
|
this.updateTransformSession(this.getPagePoint(info.point), info.shiftKey, info.altKey)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
@ -2321,8 +2314,7 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyUp = (key: string) => {
|
onKeyUp: TLKeyboardEventHandler = (key, info) => {
|
||||||
const info = this.inputs?.pointer
|
|
||||||
if (!info) return
|
if (!info) return
|
||||||
|
|
||||||
switch (this.appState.status.current) {
|
switch (this.appState.status.current) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue