@tldraw/editor 5.2.0-next.b91d4a4551c9 → 5.2.0-next.e2b8d10bf10e

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist-cjs/index.d.ts +5 -7
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/editor/Editor.js +34 -12
  4. package/dist-cjs/lib/editor/Editor.js.map +3 -3
  5. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +8 -58
  6. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
  7. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js +1 -2
  8. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +2 -2
  9. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  10. package/dist-cjs/lib/editor/types/event-types.js +0 -2
  11. package/dist-cjs/lib/editor/types/event-types.js.map +2 -2
  12. package/dist-cjs/version.js +3 -3
  13. package/dist-cjs/version.js.map +1 -1
  14. package/dist-esm/index.d.mts +5 -7
  15. package/dist-esm/index.mjs +1 -1
  16. package/dist-esm/lib/editor/Editor.mjs +34 -12
  17. package/dist-esm/lib/editor/Editor.mjs.map +3 -3
  18. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +8 -58
  19. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  20. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs +1 -2
  21. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +2 -2
  22. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  23. package/dist-esm/lib/editor/types/event-types.mjs +0 -2
  24. package/dist-esm/lib/editor/types/event-types.mjs.map +2 -2
  25. package/dist-esm/version.mjs +3 -3
  26. package/dist-esm/version.mjs.map +1 -1
  27. package/package.json +7 -7
  28. package/src/lib/editor/Editor.ts +59 -20
  29. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +54 -74
  30. package/src/lib/editor/managers/ClickManager/ClickManager.ts +15 -65
  31. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +4 -4
  32. package/src/lib/editor/managers/FocusManager/FocusManager.ts +1 -2
  33. package/src/lib/editor/tools/StateNode.ts +0 -2
  34. package/src/lib/editor/types/event-types.ts +2 -6
  35. package/src/version.ts +3 -3
@@ -122,7 +122,7 @@ describe('ClickManager', () => {
122
122
  expect(result.type).toBe('click')
123
123
  expect(result.name).toBe('double_click')
124
124
  expect(result.phase).toBe('down')
125
- expect(clickManager.clickState).toBe('pendingTriple')
125
+ expect(clickManager.clickState).toBe('pendingOverflow')
126
126
  })
127
127
 
128
128
  it('should generate double_click up event on pointer_up after double_click down', () => {
@@ -139,12 +139,34 @@ describe('ClickManager', () => {
139
139
  expect(result.phase).toBe('up')
140
140
  })
141
141
 
142
- it('should dispatch double_click settle event after timeout in pendingTriple', () => {
142
+ it('should dispatch double_click settle-down event after timeout in pendingOverflow (pointer held)', () => {
143
143
  const firstDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
144
144
  const secondDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
145
145
 
146
146
  clickManager.handlePointerEvent(firstDown)
147
147
  clickManager.handlePointerEvent(secondDown)
148
+ // no pointer_up between or after — pointer is still down at settle time
149
+
150
+ vi.advanceTimersByTime(350)
151
+
152
+ expect(editor.dispatch).toHaveBeenCalledWith(
153
+ expect.objectContaining({
154
+ type: 'click',
155
+ name: 'double_click',
156
+ phase: 'settle-down',
157
+ })
158
+ )
159
+ expect(clickManager.clickState).toBe('idle')
160
+ })
161
+
162
+ it('should dispatch double_click settle-up event after timeout in pendingOverflow (pointer released)', () => {
163
+ const down = createPointerEvent('pointer_down', { x: 100, y: 100 })
164
+ const up = createPointerEvent('pointer_up', { x: 100, y: 100 })
165
+
166
+ clickManager.handlePointerEvent(down)
167
+ clickManager.handlePointerEvent(up)
168
+ clickManager.handlePointerEvent(down)
169
+ clickManager.handlePointerEvent(up)
148
170
 
149
171
  vi.advanceTimersByTime(350)
150
172
 
@@ -152,124 +174,84 @@ describe('ClickManager', () => {
152
174
  expect.objectContaining({
153
175
  type: 'click',
154
176
  name: 'double_click',
155
- phase: 'settle',
177
+ phase: 'settle-up',
156
178
  })
157
179
  )
158
180
  expect(clickManager.clickState).toBe('idle')
159
181
  })
160
182
  })
161
183
 
162
- describe('triple and quadruple click detection', () => {
163
- it('should detect triple click on third pointer_down', () => {
184
+ describe('overflow click handling', () => {
185
+ it('should enter overflow on the third pointer_down without emitting another click', () => {
164
186
  const firstDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
165
187
  const secondDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
166
188
  const thirdDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
167
189
 
168
190
  clickManager.handlePointerEvent(firstDown)
169
191
  clickManager.handlePointerEvent(secondDown)
170
- const result = clickManager.handlePointerEvent(thirdDown) as TLClickEventInfo
192
+ const result = clickManager.handlePointerEvent(thirdDown)
171
193
 
172
- expect(result.type).toBe('click')
173
- expect(result.name).toBe('triple_click')
174
- expect(result.phase).toBe('down')
175
- expect(clickManager.clickState).toBe('pendingQuadruple')
194
+ expect(result).toBe(thirdDown)
195
+ expect(clickManager.clickState).toBe('overflow')
176
196
  })
177
197
 
178
- it('should detect quadruple click on fourth pointer_down', () => {
198
+ it('should keep overflow active on further clicks', () => {
179
199
  const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
180
200
 
181
201
  clickManager.handlePointerEvent(pointerDown) // first
182
202
  clickManager.handlePointerEvent(pointerDown) // second (double_click)
183
- clickManager.handlePointerEvent(pointerDown) // third (triple_click)
184
- const result = clickManager.handlePointerEvent(pointerDown) as TLClickEventInfo // fourth
185
-
186
- expect(result.type).toBe('click')
187
- expect(result.name).toBe('quadruple_click')
188
- expect(result.phase).toBe('down')
189
- expect(clickManager.clickState).toBe('pendingOverflow')
190
- })
191
-
192
- it('should handle overflow state after quadruple click', () => {
193
- const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
194
-
195
- clickManager.handlePointerEvent(pointerDown) // first
196
- clickManager.handlePointerEvent(pointerDown) // second
197
- clickManager.handlePointerEvent(pointerDown) // third
198
- clickManager.handlePointerEvent(pointerDown) // fourth
199
- const result = clickManager.handlePointerEvent(pointerDown) // fifth
203
+ clickManager.handlePointerEvent(pointerDown) // third (overflow)
204
+ const result = clickManager.handlePointerEvent(pointerDown) // fourth
200
205
 
201
206
  expect(result).toBe(pointerDown)
202
207
  expect(clickManager.clickState).toBe('overflow')
203
208
  })
204
209
 
205
- it('should generate triple_click up event on pointer_up after triple_click down', () => {
206
- const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
207
- const pointerUp = createPointerEvent('pointer_up', { x: 100, y: 100 })
208
-
209
- clickManager.handlePointerEvent(pointerDown) // first
210
- clickManager.handlePointerEvent(pointerDown) // second
211
- clickManager.handlePointerEvent(pointerDown) // third
212
- const result = clickManager.handlePointerEvent(pointerUp) as TLClickEventInfo
213
-
214
- expect(result.type).toBe('click')
215
- expect(result.name).toBe('triple_click')
216
- expect(result.phase).toBe('up')
217
- })
218
-
219
- it('should generate quadruple_click up event on pointer_up after quadruple_click down', () => {
210
+ it('should not emit double_click up events while in overflow', () => {
220
211
  const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
221
212
  const pointerUp = createPointerEvent('pointer_up', { x: 100, y: 100 })
222
213
 
223
214
  clickManager.handlePointerEvent(pointerDown) // first
224
215
  clickManager.handlePointerEvent(pointerDown) // second
225
216
  clickManager.handlePointerEvent(pointerDown) // third
226
- clickManager.handlePointerEvent(pointerDown) // fourth
227
- const result = clickManager.handlePointerEvent(pointerUp) as TLClickEventInfo
217
+ const result = clickManager.handlePointerEvent(pointerUp)
228
218
 
229
- expect(result.type).toBe('click')
230
- expect(result.name).toBe('quadruple_click')
231
- expect(result.phase).toBe('up')
219
+ expect(result).toBe(pointerUp)
220
+ expect(clickManager.clickState).toBe('overflow')
232
221
  })
233
- })
234
222
 
235
- describe('timeout behavior and settle events', () => {
236
- it('should dispatch triple_click settle event after timeout in pendingQuadruple', () => {
223
+ it('should return to idle after overflow timeout without dispatching a settle event', () => {
237
224
  const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
238
225
 
239
226
  clickManager.handlePointerEvent(pointerDown) // first
240
227
  clickManager.handlePointerEvent(pointerDown) // second
241
- clickManager.handlePointerEvent(pointerDown) // third
228
+ clickManager.handlePointerEvent(pointerDown) // third -> overflow
242
229
 
243
230
  vi.advanceTimersByTime(350)
244
231
 
245
- expect(editor.dispatch).toHaveBeenCalledWith(
246
- expect.objectContaining({
247
- type: 'click',
248
- name: 'triple_click',
249
- phase: 'settle',
250
- })
251
- )
232
+ expect(editor.dispatch).not.toHaveBeenCalled()
252
233
  expect(clickManager.clickState).toBe('idle')
253
234
  })
235
+ })
254
236
 
255
- it('should dispatch quadruple_click settle event after timeout in pendingOverflow', () => {
256
- const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
237
+ describe('timeout behavior and settle events', () => {
238
+ it('should track press/release state across the pending window (settle-down then release → settle-up)', () => {
239
+ const down = createPointerEvent('pointer_down', { x: 100, y: 100 })
240
+ const up = createPointerEvent('pointer_up', { x: 100, y: 100 })
257
241
 
258
- clickManager.handlePointerEvent(pointerDown) // first
259
- clickManager.handlePointerEvent(pointerDown) // second
260
- clickManager.handlePointerEvent(pointerDown) // third
261
- clickManager.handlePointerEvent(pointerDown) // fourth
242
+ clickManager.handlePointerEvent(down)
243
+ clickManager.handlePointerEvent(up)
244
+ clickManager.handlePointerEvent(down) // second press — pointer is down...
245
+ clickManager.handlePointerEvent(up) // ...but released before timeout
262
246
 
263
247
  vi.advanceTimersByTime(350)
264
248
 
265
249
  expect(editor.dispatch).toHaveBeenCalledWith(
266
250
  expect.objectContaining({
267
- type: 'click',
268
- name: 'quadruple_click',
269
- phase: 'settle',
251
+ name: 'double_click',
252
+ phase: 'settle-up',
270
253
  })
271
254
  )
272
- expect(clickManager.clickState).toBe('idle')
273
255
  })
274
256
 
275
257
  it('should use different timeout durations for different states', () => {
@@ -316,7 +298,7 @@ describe('ClickManager', () => {
316
298
 
317
299
  expect(result.type).toBe('click')
318
300
  expect(result.name).toBe('double_click')
319
- expect(clickManager.clickState).toBe('pendingTriple')
301
+ expect(clickManager.clickState).toBe('pendingOverflow')
320
302
  })
321
303
  })
322
304
 
@@ -396,7 +378,7 @@ describe('ClickManager', () => {
396
378
 
397
379
  clickManager.handlePointerEvent(pointerDown)
398
380
  clickManager.handlePointerEvent(pointerDown) // double click
399
- expect(clickManager.clickState).toBe('pendingTriple')
381
+ expect(clickManager.clickState).toBe('pendingOverflow')
400
382
 
401
383
  clickManager.cancelDoubleClickTimeout()
402
384
 
@@ -416,9 +398,7 @@ describe('ClickManager', () => {
416
398
  // Get to overflow state
417
399
  clickManager.handlePointerEvent(pointerDown) // 1
418
400
  clickManager.handlePointerEvent(pointerDown) // 2
419
- clickManager.handlePointerEvent(pointerDown) // 3
420
- clickManager.handlePointerEvent(pointerDown) // 4
421
- clickManager.handlePointerEvent(pointerDown) // 5 -> overflow
401
+ clickManager.handlePointerEvent(pointerDown) // 3 -> overflow
422
402
 
423
403
  expect(clickManager.clickState).toBe('overflow')
424
404
 
@@ -4,13 +4,7 @@ import type { Editor } from '../../Editor'
4
4
  import { TLClickEventInfo, TLPointerEventInfo } from '../../types/event-types'
5
5
 
6
6
  /** @public */
7
- export type TLClickState =
8
- | 'idle'
9
- | 'pendingDouble'
10
- | 'pendingTriple'
11
- | 'pendingQuadruple'
12
- | 'pendingOverflow'
13
- | 'overflow'
7
+ export type TLClickState = 'idle' | 'pendingDouble' | 'pendingOverflow' | 'overflow'
14
8
 
15
9
  const MAX_CLICK_DISTANCE = 40
16
10
 
@@ -26,6 +20,8 @@ export class ClickManager {
26
20
 
27
21
  private _previousScreenPoint?: Vec
28
22
 
23
+ private _isPressingWhilePending = false
24
+
29
25
  @bind
30
26
  _getClickTimeout(state: TLClickState, id = uniqueId()) {
31
27
  this._clickId = id
@@ -34,30 +30,12 @@ export class ClickManager {
34
30
  () => {
35
31
  if (this._clickState === state && this._clickId === id) {
36
32
  switch (this._clickState) {
37
- case 'pendingTriple': {
38
- this.editor.dispatch({
39
- ...this.lastPointerInfo,
40
- type: 'click',
41
- name: 'double_click',
42
- phase: 'settle',
43
- })
44
- break
45
- }
46
- case 'pendingQuadruple': {
47
- this.editor.dispatch({
48
- ...this.lastPointerInfo,
49
- type: 'click',
50
- name: 'triple_click',
51
- phase: 'settle',
52
- })
53
- break
54
- }
55
33
  case 'pendingOverflow': {
56
34
  this.editor.dispatch({
57
35
  ...this.lastPointerInfo,
58
36
  type: 'click',
59
- name: 'quadruple_click',
60
- phase: 'settle',
37
+ name: 'double_click',
38
+ phase: this._isPressingWhilePending ? 'settle-down' : 'settle-up',
61
39
  })
62
40
  break
63
41
  }
@@ -100,6 +78,8 @@ export class ClickManager {
100
78
  if (!this._clickState) return info
101
79
  this._clickScreenPoint = Vec.From(info.point)
102
80
 
81
+ this._isPressingWhilePending = true
82
+
103
83
  if (
104
84
  this._previousScreenPoint &&
105
85
  Vec.Dist2(this._previousScreenPoint, this._clickScreenPoint) > MAX_CLICK_DISTANCE ** 2
@@ -113,32 +93,12 @@ export class ClickManager {
113
93
 
114
94
  switch (this._clickState) {
115
95
  case 'pendingDouble': {
116
- this._clickState = 'pendingTriple'
117
- this._clickTimeout = this._getClickTimeout(this._clickState)
118
- return {
119
- ...info,
120
- type: 'click',
121
- name: 'double_click',
122
- phase: 'down',
123
- }
124
- }
125
- case 'pendingTriple': {
126
- this._clickState = 'pendingQuadruple'
127
- this._clickTimeout = this._getClickTimeout(this._clickState)
128
- return {
129
- ...info,
130
- type: 'click',
131
- name: 'triple_click',
132
- phase: 'down',
133
- }
134
- }
135
- case 'pendingQuadruple': {
136
96
  this._clickState = 'pendingOverflow'
137
97
  this._clickTimeout = this._getClickTimeout(this._clickState)
138
98
  return {
139
99
  ...info,
140
100
  type: 'click',
141
- name: 'quadruple_click',
101
+ name: 'double_click',
142
102
  phase: 'down',
143
103
  }
144
104
  }
@@ -159,30 +119,17 @@ export class ClickManager {
159
119
  }
160
120
  case 'pointer_up': {
161
121
  if (!this._clickState) return info
122
+
162
123
  this._clickScreenPoint = Vec.From(info.point)
163
124
 
125
+ this._isPressingWhilePending = false
126
+
164
127
  switch (this._clickState) {
165
- case 'pendingTriple': {
166
- return {
167
- ...this.lastPointerInfo,
168
- type: 'click',
169
- name: 'double_click',
170
- phase: 'up',
171
- }
172
- }
173
- case 'pendingQuadruple': {
174
- return {
175
- ...this.lastPointerInfo,
176
- type: 'click',
177
- name: 'triple_click',
178
- phase: 'up',
179
- }
180
- }
181
128
  case 'pendingOverflow': {
182
129
  return {
183
130
  ...this.lastPointerInfo,
184
131
  type: 'click',
185
- name: 'quadruple_click',
132
+ name: 'double_click',
186
133
  phase: 'up',
187
134
  }
188
135
  }
@@ -219,5 +166,8 @@ export class ClickManager {
219
166
  cancelDoubleClickTimeout() {
220
167
  this._clickTimeout = clearTimeout(this._clickTimeout)
221
168
  this._clickState = 'idle'
169
+ // when a double click is cancelled, we are no longer pending any further
170
+ // clicks, so we set this to false even if the user is still pressing
171
+ this._isPressingWhilePending = false
222
172
  }
223
173
  }
@@ -14,7 +14,7 @@ describe('FocusManager', () => {
14
14
  getInstanceState: Mock
15
15
  updateInstanceState: Mock
16
16
  getContainer: Mock
17
- isIn: Mock
17
+ getEditingShapeId: Mock
18
18
  getSelectedShapeIds: Mock
19
19
  complete: Mock
20
20
  }
@@ -51,7 +51,7 @@ describe('FocusManager', () => {
51
51
  updateInstanceState: vi.fn(),
52
52
  getContainer: vi.fn(() => mockContainer),
53
53
  getContainerDocument: vi.fn(() => document),
54
- isIn: vi.fn(() => false),
54
+ getEditingShapeId: vi.fn(() => null),
55
55
  getSelectedShapeIds: vi.fn(() => []),
56
56
  complete: vi.fn(),
57
57
  } as any
@@ -243,7 +243,7 @@ describe('FocusManager', () => {
243
243
  })
244
244
 
245
245
  it('should return early when editor is in editing mode', () => {
246
- editor.isIn.mockReturnValue(true)
246
+ editor.getEditingShapeId.mockReturnValue('shape:1')
247
247
  const event = new KeyboardEvent('keydown', { key: 'Tab' })
248
248
 
249
249
  keydownHandler(event)
@@ -407,7 +407,7 @@ describe('FocusManager', () => {
407
407
  const keydownCall = addEventListenerCalls.find((call: any) => call[0] === 'keydown')
408
408
  const keydownHandler = keydownCall![1]
409
409
 
410
- editor.isIn.mockReturnValue(true) // Editing mode
410
+ editor.getEditingShapeId.mockReturnValue('shape:1') // Editing mode
411
411
 
412
412
  const event = new KeyboardEvent('keydown', { key: 'Tab' })
413
413
  keydownHandler(event)
@@ -63,8 +63,7 @@ export class FocusManager {
63
63
  const activeEl = container.ownerDocument.activeElement
64
64
  // Edit mode should remove the focus ring, however if the active element's
65
65
  // parent is the contextual toolbar, then allow it.
66
- if (this.editor.isIn('select.editing_shape') && !activeEl?.closest('.tlui-contextual-toolbar'))
67
- return
66
+ if (this.editor.getEditingShapeId() && !activeEl?.closest('.tlui-contextual-toolbar')) return
68
67
  if (activeEl === container && this.editor.getSelectedShapeIds().length > 0) return
69
68
  if (['Tab', 'ArrowUp', 'ArrowDown'].includes(keyEvent.key)) {
70
69
  container.classList.remove('tl-container__no-focus-ring')
@@ -268,8 +268,6 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
268
268
  onLongPress?(info: TLPointerEventInfo): void
269
269
  onPointerUp?(info: TLPointerEventInfo): void
270
270
  onDoubleClick?(info: TLClickEventInfo): void
271
- onTripleClick?(info: TLClickEventInfo): void
272
- onQuadrupleClick?(info: TLClickEventInfo): void
273
271
  onRightClick?(info: TLPointerEventInfo): void
274
272
  onMiddleClick?(info: TLPointerEventInfo): void
275
273
  onKeyDown?(info: TLKeyboardEventInfo): void
@@ -24,7 +24,7 @@ export type TLPointerEventName =
24
24
  | 'middle_click'
25
25
 
26
26
  /** @public */
27
- export type TLCLickEventName = 'double_click' | 'triple_click' | 'quadruple_click'
27
+ export type TLCLickEventName = 'double_click'
28
28
 
29
29
  /** @public */
30
30
  export type TLPinchEventName = 'pinch_start' | 'pinch' | 'pinch_end'
@@ -72,7 +72,7 @@ export type TLClickEventInfo = TLBaseEventInfo & {
72
72
  point: VecLike
73
73
  pointerId: number
74
74
  button: number
75
- phase: 'down' | 'up' | 'settle'
75
+ phase: 'down' | 'up' | 'settle-down' | 'settle-up'
76
76
  } & TLPointerEventTarget
77
77
 
78
78
  /** @public */
@@ -173,8 +173,6 @@ export interface TLEventHandlers {
173
173
  onLongPress: TLPointerEvent
174
174
  onRightClick: TLPointerEvent
175
175
  onDoubleClick: TLClickEvent
176
- onTripleClick: TLClickEvent
177
- onQuadrupleClick: TLClickEvent
178
176
  onMiddleClick: TLPointerEvent
179
177
  onPointerUp: TLPointerEvent
180
178
  onKeyDown: TLKeyboardEvent
@@ -206,7 +204,5 @@ export const EVENT_NAME_MAP: Record<
206
204
  complete: 'onComplete',
207
205
  interrupt: 'onInterrupt',
208
206
  double_click: 'onDoubleClick',
209
- triple_click: 'onTripleClick',
210
- quadruple_click: 'onQuadrupleClick',
211
207
  tick: 'onTick',
212
208
  }
package/src/version.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  // This file is automatically generated by internal/scripts/refresh-assets.ts.
2
2
  // Do not edit manually. Or do, I'm a comment, not a cop.
3
3
 
4
- export const version = '5.2.0-next.b91d4a4551c9'
4
+ export const version = '5.2.0-next.e2b8d10bf10e'
5
5
  export const publishDates = {
6
6
  major: '2026-05-06T16:28:18.473Z',
7
- minor: '2026-06-03T10:38:24.758Z',
8
- patch: '2026-06-03T10:38:24.758Z',
7
+ minor: '2026-06-05T16:06:49.927Z',
8
+ patch: '2026-06-05T16:06:49.927Z',
9
9
  }