Fix weird text complete (#3342)

This PR fixes a weird issue where editing text notes would call blur /
complete, preventing certain interactions.

Before:
1. Select a sticky note
2. Start editing the note
3. pointer down on the canvas
4. Instead of being in pointing_canvas, you'll be in idle

After:
4. You'll be in pointing_canvas

### Change Type

- [x] `sdk` — Changes the tldraw SDK
- [x] `bugfix` — Bug fix
pull/3345/head
Steve Ruiz 2024-04-03 16:17:40 +01:00 zatwierdzone przez GitHub
rodzic 30e605ef7e
commit e60f8d56a0
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
4 zmienionych plików z 65 dodań i 66 usunięć

Wyświetl plik

@ -795,6 +795,7 @@ export class Editor extends EventEmitter<TLEventMap> {
* @public
*/
undo(): this {
this._flushEventsForTick(0)
this.history.undo()
return this
}
@ -819,6 +820,7 @@ export class Editor extends EventEmitter<TLEventMap> {
* @public
*/
redo(): this {
this._flushEventsForTick(0)
this.history.redo()
return this
}
@ -8180,7 +8182,7 @@ export class Editor extends EventEmitter<TLEventMap> {
const sx = info.point.x - screenBounds.x
const sy = info.point.y - screenBounds.y
const sz = info.point.z
const sz = info.point.z ?? 0.5
previousScreenPoint.setTo(currentScreenPoint)
previousPagePoint.setTo(currentPagePoint)
@ -8190,7 +8192,7 @@ export class Editor extends EventEmitter<TLEventMap> {
// it will be 0,0 when its actual screen position is equal
// to screenBounds.point. This is confusing!
currentScreenPoint.set(sx, sy)
currentPagePoint.set(sx / cz - cx, sy / cz - cy, sz ?? 0.5)
currentPagePoint.set(sx / cz - cx, sy / cz - cy, sz)
this.inputs.isPen = info.type === 'pointer' && info.isPen
@ -8367,6 +8369,9 @@ export class Editor extends EventEmitter<TLEventMap> {
*/
dispatch = (info: TLEventInfo): this => {
this._pendingEventsForNextTick.push(info)
if (!(info.type === 'pointer' || info.type === 'wheel' || info.type === 'pinch')) {
this._flushEventsForTick(0)
}
return this
}

Wyświetl plik

@ -1,11 +1,14 @@
import { Vec } from '@tldraw/editor'
import { Box, Vec } from '@tldraw/editor'
import { TestEditor } from '../../../test/TestEditor'
let editor: TestEditor
beforeEach(() => {
editor = new TestEditor()
// We don't want the camera to move when the shape gets created off screen
editor.updateViewportScreenBounds(new Box(0, 0, 2000, 2000))
})
afterEach(() => {
editor?.dispose()
})
@ -24,8 +27,14 @@ function testCloneHandles(x: number, y: number, rotation: number) {
)
handles.forEach((handle, i) => {
const handleInPageSpace = editor.getShapePageTransform(shape).applyToPoint(handle)
editor.select(shape.id)
editor.pointerDown(handle.x, handle.y, {
editor.pointerMove(handleInPageSpace.x, handleInPageSpace.y)
expect(editor.inputs.currentPagePoint).toMatchObject({
x: handleInPageSpace.x,
y: handleInPageSpace.y,
})
editor.pointerDown(handleInPageSpace.x, handleInPageSpace.y, {
target: 'handle',
shape,
handle,
@ -50,25 +59,21 @@ function testCloneHandles(x: number, y: number, rotation: number) {
editor.expectToBeIn('select.editing_shape')
editor.cancel().undo()
editor.cancel().undo().forceTick()
})
}
describe('Note clone handles', () => {
it('Creates a new sticky note using handles', () => {
testCloneHandles(0, 0, 0)
})
it('Creates a new sticky note when translated', () => {
testCloneHandles(100, 100, 0)
testCloneHandles(1000, 1000, 0)
})
it('Creates a new sticky when rotated', () => {
testCloneHandles(0, 0, Math.PI / 2)
testCloneHandles(1000, 1000, Math.PI / 2)
})
it('Creates a new sticky when translated and rotated', () => {
testCloneHandles(100, 100, Math.PI / 2)
testCloneHandles(1000, 1000, Math.PI / 2)
})
})
@ -82,9 +87,10 @@ function testDragCloneHandles(x: number, y: number, rotation: number) {
const handles = editor.getShapeHandles(shape.id)!
handles.forEach((handle) => {
const handleInPageSpace = editor.getShapePageTransform(shape).applyToPoint(handle)
editor.select(shape.id)
editor.pointerMove(handle.x, handle.y)
editor.pointerDown(handle.x, handle.y, {
editor.pointerMove(handleInPageSpace.x, handleInPageSpace.y)
editor.pointerDown(handleInPageSpace.x, handleInPageSpace.y, {
target: 'handle',
shape,
handle,
@ -92,7 +98,7 @@ function testDragCloneHandles(x: number, y: number, rotation: number) {
editor.expectToBeIn('select.pointing_handle')
editor.pointerMove(handle.x + 30, handle.y + 30)
editor.pointerMove(handleInPageSpace.x + 30, handleInPageSpace.y + 30)
editor.expectToBeIn('select.translating')
@ -105,8 +111,8 @@ function testDragCloneHandles(x: number, y: number, rotation: number) {
editor.expectShapeToMatch({
id: newShape.id,
type: 'note',
x: handle.x + 30 - offset.x,
y: handle.y + 30 - offset.y,
x: handleInPageSpace.x + 30 - offset.x,
y: handleInPageSpace.y + 30 - offset.y,
})
editor.pointerUp()
@ -119,27 +125,23 @@ function testDragCloneHandles(x: number, y: number, rotation: number) {
describe('Dragging clone handles', () => {
it('Creates a new sticky note using handles', () => {
testDragCloneHandles(0, 0, 0)
})
it('Creates a new sticky note when translated', () => {
testDragCloneHandles(100, 100, 0)
testDragCloneHandles(1000, 1000, 0)
})
it('Creates a new sticky when rotated', () => {
testDragCloneHandles(0, 0, Math.PI / 2)
testDragCloneHandles(1000, 1000, Math.PI / 2)
})
it('Creates a new sticky when translated and rotated', () => {
testDragCloneHandles(100, 100, Math.PI / 2)
testDragCloneHandles(1000, 1000, Math.PI / 2)
})
})
it('Selects an adjacent note when clicking the clone handle', () => {
editor.createShape({ type: 'note', x: 220, y: 0 })
editor.createShape({ type: 'note', x: 1220, y: 1000 })
const shapeA = editor.getLastCreatedShape()!
editor.createShape({ type: 'note', x: 0, y: 0 })
editor.createShape({ type: 'note', x: 1000, y: 1000 })
const shapeB = editor.getLastCreatedShape()!
editor.select(shapeB.id)
@ -171,10 +173,10 @@ it('Selects an adjacent note when clicking the clone handle', () => {
})
it('Creates an adjacent note when dragging the clone handle', () => {
editor.createShape({ type: 'note', x: 220, y: 0 })
editor.createShape({ type: 'note', x: 1220, y: 1000 })
const shapeA = editor.getLastCreatedShape()!
editor.createShape({ type: 'note', x: 0, y: 0 })
editor.createShape({ type: 'note', x: 1000, y: 1000 })
const shapeB = editor.getLastCreatedShape()!
editor.select(shapeB.id)
@ -214,10 +216,10 @@ it('Creates an adjacent note when dragging the clone handle', () => {
})
it('Does not put the new shape into a frame if its center is not in the frame', () => {
editor.createShape({ type: 'frame', x: 321, y: 100 }) // one pixel too far...
editor.createShape({ type: 'frame', x: 1321, y: 1000 }) // one pixel too far...
const frameA = editor.getLastCreatedShape()!
// center no longer in the frame
editor.createShape({ type: 'note', x: 0, y: 0 })
editor.createShape({ type: 'note', x: 1000, y: 1000 })
const shapeA = editor.getLastCreatedShape()!
// to the right
const handle = editor.getShapeHandles(shapeA.id)![1]
@ -237,10 +239,10 @@ it('Does not put the new shape into a frame if its center is not in the frame',
})
it('Puts the new shape into a frame based on its center', () => {
editor.createShape({ type: 'frame', x: 320, y: 100 })
editor.createShape({ type: 'frame', x: 1320, y: 1100 })
const frameA = editor.getLastCreatedShape()!
// top left won't be in the frame, but the center will (barely but yes)
editor.createShape({ type: 'note', x: 0, y: 0 })
editor.createShape({ type: 'note', x: 1000, y: 1000 })
const shapeA = editor.getLastCreatedShape()!
// to the right
const handle = editor.getShapeHandles(shapeA.id)![1]
@ -260,10 +262,10 @@ it('Puts the new shape into a frame based on its center', () => {
})
function testNoteShapeFrameRotations(sourceRotation: number, rotation: number) {
editor.createShape({ type: 'frame', x: 220, y: 0, rotation: rotation })
editor.createShape({ type: 'frame', x: 1220, y: 1000, rotation: rotation })
const frameA = editor.getLastCreatedShape()!
// top left won't be in the frame, but the center will (barely but yes)
editor.createShape({ type: 'note', x: 0, y: 0, rotation: sourceRotation })
editor.createShape({ type: 'note', x: 1000, y: 1000, rotation: sourceRotation })
const shapeA = editor.getLastCreatedShape()!
// to the right
const handle = editor.getShapeHandles(shapeA.id)![1]

Wyświetl plik

@ -172,15 +172,7 @@ export function getNoteShapeForAdjacentPosition(
nextNote = editor.getShape(id)!
}
// Animate to the next sticky if it would be off screen
const selectionPageBounds = editor.getSelectionPageBounds()
const viewportPageBounds = editor.getViewportPageBounds()
if (selectionPageBounds && !viewportPageBounds.contains(selectionPageBounds)) {
editor.centerOnPoint(selectionPageBounds.center, {
duration: ANIMATION_MEDIUM_MS,
})
}
zoomToShapeIfOffscreen(editor)
return nextNote
}
@ -197,9 +189,14 @@ export function startEditingNoteShape(editor: Editor, shape: TLShape) {
// Select any text that's in the newly selected sticky
;(document.getElementById(`text-input-${shape.id}`) as HTMLTextAreaElement)?.select()
zoomToShapeIfOffscreen(editor)
}
function zoomToShapeIfOffscreen(editor: Editor) {
const selectionPageBounds = editor.getSelectionPageBounds()
const viewportPageBounds = editor.getViewportPageBounds()
if (selectionPageBounds && !viewportPageBounds.contains(selectionPageBounds)) {
console.log('centering on point', viewportPageBounds, selectionPageBounds)
editor.centerOnPoint(selectionPageBounds.center, {
duration: ANIMATION_MEDIUM_MS,
})

Wyświetl plik

@ -74,32 +74,27 @@ export function useEditableText(id: TLShapeId, type: string, text: string) {
const elm = rInput.current
const editingShapeId = editor.getEditingShapeId()
// Did we move to a different shape?
if (editingShapeId) {
// important! these ^v are two different things
// is that shape OUR shape?
if (elm && editingShapeId === id) {
if (ranges) {
if (!ranges.length) {
// If we don't have any ranges, restore selection
// and select all of the text
elm.focus()
} else {
// Otherwise, skip the select-all-on-focus behavior
// and restore the selection
rSkipSelectOnFocus.current = true
elm.focus()
const selection = window.getSelection()
if (selection) {
ranges.forEach((range) => selection.addRange(range))
}
}
} else {
// important! these ^v are two different things
// is that shape OUR shape?
if (elm && editingShapeId === id) {
if (ranges) {
if (!ranges.length) {
// If we don't have any ranges, restore selection
// and select all of the text
elm.focus()
} else {
// Otherwise, skip the select-all-on-focus behavior
// and restore the selection
rSkipSelectOnFocus.current = true
elm.focus()
const selection = window.getSelection()
if (selection) {
ranges.forEach((range) => selection.addRange(range))
}
}
} else {
elm.focus()
}
} else {
window.getSelection()?.removeAllRanges()
editor.complete()
}
})
}, [editor, id])