@tldraw/editor 3.14.0-canary.e099968e8b09 → 3.14.0-canary.e2a8e4a03aff

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 (162) hide show
  1. package/dist-cjs/index.d.ts +74 -63
  2. package/dist-cjs/index.js +8 -10
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/config/TLSessionStateSnapshot.js +1 -12
  5. package/dist-cjs/lib/config/TLSessionStateSnapshot.js.map +3 -3
  6. package/dist-cjs/lib/editor/Editor.js +80 -73
  7. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  8. package/dist-cjs/lib/editor/bindings/BindingUtil.js.map +2 -2
  9. package/dist-cjs/lib/editor/derivations/bindingsIndex.js +22 -22
  10. package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
  11. package/dist-cjs/lib/editor/derivations/parentsToChildren.js +16 -16
  12. package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
  13. package/dist-cjs/lib/editor/managers/{ClickManager.js → ClickManager/ClickManager.js} +1 -1
  14. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +7 -0
  15. package/dist-cjs/lib/editor/managers/{EdgeScrollManager.js → EdgeScrollManager/EdgeScrollManager.js} +2 -2
  16. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +7 -0
  17. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +7 -0
  18. package/dist-cjs/lib/editor/managers/{FontManager.js → FontManager/FontManager.js} +1 -2
  19. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +7 -0
  20. package/dist-cjs/lib/editor/managers/{HistoryManager.js → HistoryManager/HistoryManager.js} +64 -6
  21. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +7 -0
  22. package/dist-cjs/lib/editor/managers/{ScribbleManager.js → ScribbleManager/ScribbleManager.js} +1 -1
  23. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +7 -0
  24. package/dist-cjs/lib/editor/managers/{TextManager.js → TextManager/TextManager.js} +73 -42
  25. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +7 -0
  26. package/dist-cjs/lib/editor/managers/{TickManager.js → TickManager/TickManager.js} +1 -1
  27. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +7 -0
  28. package/dist-cjs/lib/editor/managers/{UserPreferencesManager.js → UserPreferencesManager/UserPreferencesManager.js} +1 -1
  29. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +7 -0
  30. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +1 -1
  31. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +1 -1
  32. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +1 -1
  33. package/dist-cjs/lib/editor/tools/StateNode.js +3 -3
  34. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  35. package/dist-cjs/lib/editor/types/emit-types.js.map +1 -1
  36. package/dist-cjs/lib/editor/types/external-content.js.map +1 -1
  37. package/dist-cjs/lib/exports/getSvgJsx.js.map +1 -1
  38. package/dist-cjs/lib/hooks/useCanvasEvents.js +1 -2
  39. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  40. package/dist-cjs/lib/primitives/Box.js +0 -6
  41. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  42. package/dist-cjs/lib/utils/areShapesContentEqual.js +1 -1
  43. package/dist-cjs/lib/utils/areShapesContentEqual.js.map +2 -2
  44. package/dist-cjs/lib/utils/reorderShapes.js +11 -10
  45. package/dist-cjs/lib/utils/reorderShapes.js.map +2 -2
  46. package/dist-cjs/lib/utils/richText.js +7 -2
  47. package/dist-cjs/lib/utils/richText.js.map +2 -2
  48. package/dist-cjs/version.js +3 -3
  49. package/dist-cjs/version.js.map +1 -1
  50. package/dist-esm/index.d.mts +74 -63
  51. package/dist-esm/index.mjs +12 -10
  52. package/dist-esm/index.mjs.map +2 -2
  53. package/dist-esm/lib/config/TLSessionStateSnapshot.mjs +1 -1
  54. package/dist-esm/lib/config/TLSessionStateSnapshot.mjs.map +2 -2
  55. package/dist-esm/lib/editor/Editor.mjs +80 -73
  56. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  57. package/dist-esm/lib/editor/bindings/BindingUtil.mjs.map +2 -2
  58. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs +22 -22
  59. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
  60. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +16 -16
  61. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
  62. package/dist-esm/lib/editor/managers/{ClickManager.mjs → ClickManager/ClickManager.mjs} +1 -1
  63. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +7 -0
  64. package/dist-esm/lib/editor/managers/{EdgeScrollManager.mjs → EdgeScrollManager/EdgeScrollManager.mjs} +2 -2
  65. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +7 -0
  66. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +7 -0
  67. package/dist-esm/lib/editor/managers/{FontManager.mjs → FontManager/FontManager.mjs} +1 -2
  68. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +7 -0
  69. package/dist-esm/lib/editor/managers/{HistoryManager.mjs → HistoryManager/HistoryManager.mjs} +60 -2
  70. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +7 -0
  71. package/dist-esm/lib/editor/managers/{ScribbleManager.mjs → ScribbleManager/ScribbleManager.mjs} +1 -1
  72. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +7 -0
  73. package/dist-esm/lib/editor/managers/{TextManager.mjs → TextManager/TextManager.mjs} +73 -42
  74. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +7 -0
  75. package/dist-esm/lib/editor/managers/{TickManager.mjs → TickManager/TickManager.mjs} +1 -1
  76. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +7 -0
  77. package/dist-esm/lib/editor/managers/{UserPreferencesManager.mjs → UserPreferencesManager/UserPreferencesManager.mjs} +1 -1
  78. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +7 -0
  79. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +1 -1
  80. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +1 -1
  81. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +1 -1
  82. package/dist-esm/lib/editor/tools/StateNode.mjs +3 -3
  83. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  84. package/dist-esm/lib/exports/getSvgJsx.mjs.map +1 -1
  85. package/dist-esm/lib/hooks/useCanvasEvents.mjs +1 -2
  86. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  87. package/dist-esm/lib/primitives/Box.mjs +0 -6
  88. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  89. package/dist-esm/lib/utils/areShapesContentEqual.mjs +1 -1
  90. package/dist-esm/lib/utils/areShapesContentEqual.mjs.map +2 -2
  91. package/dist-esm/lib/utils/reorderShapes.mjs +11 -10
  92. package/dist-esm/lib/utils/reorderShapes.mjs.map +2 -2
  93. package/dist-esm/lib/utils/richText.mjs +8 -3
  94. package/dist-esm/lib/utils/richText.mjs.map +2 -2
  95. package/dist-esm/version.mjs +3 -3
  96. package/dist-esm/version.mjs.map +1 -1
  97. package/editor.css +433 -482
  98. package/package.json +8 -9
  99. package/src/index.ts +15 -8
  100. package/src/lib/config/TLSessionStateSnapshot.ts +1 -1
  101. package/src/lib/editor/Editor.test.ts +252 -3
  102. package/src/lib/editor/Editor.ts +81 -72
  103. package/src/lib/editor/bindings/BindingUtil.ts +6 -0
  104. package/src/lib/editor/derivations/bindingsIndex.ts +27 -26
  105. package/src/lib/editor/derivations/parentsToChildren.ts +28 -25
  106. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +442 -0
  107. package/src/lib/editor/managers/{ClickManager.ts → ClickManager/ClickManager.ts} +3 -3
  108. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +374 -0
  109. package/src/lib/editor/managers/{EdgeScrollManager.ts → EdgeScrollManager/EdgeScrollManager.ts} +3 -3
  110. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +455 -0
  111. package/src/lib/editor/managers/{FocusManager.ts → FocusManager/FocusManager.ts} +1 -1
  112. package/src/lib/editor/managers/FontManager/FontManager.test.ts +263 -0
  113. package/src/lib/editor/managers/{FontManager.ts → FontManager/FontManager.ts} +2 -3
  114. package/src/lib/editor/managers/{HistoryManager.test.ts → HistoryManager/HistoryManager.test.ts} +388 -1
  115. package/src/lib/editor/managers/{HistoryManager.ts → HistoryManager/HistoryManager.ts} +73 -2
  116. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +624 -0
  117. package/src/lib/editor/managers/{ScribbleManager.ts → ScribbleManager/ScribbleManager.ts} +2 -2
  118. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +485 -0
  119. package/src/lib/editor/managers/TextManager/TextManager.test.ts +407 -0
  120. package/src/lib/editor/managers/{TextManager.ts → TextManager/TextManager.ts} +119 -87
  121. package/src/lib/editor/managers/TickManager/TickManager.test.ts +314 -0
  122. package/src/lib/editor/managers/{TickManager.ts → TickManager/TickManager.ts} +2 -2
  123. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +591 -0
  124. package/src/lib/editor/managers/{UserPreferencesManager.ts → UserPreferencesManager/UserPreferencesManager.ts} +2 -2
  125. package/src/lib/editor/shapes/ShapeUtil.ts +2 -1
  126. package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +1 -1
  127. package/src/lib/editor/tools/StateNode.ts +3 -3
  128. package/src/lib/editor/types/emit-types.ts +4 -0
  129. package/src/lib/editor/types/external-content.ts +11 -2
  130. package/src/lib/exports/getSvgJsx.tsx +1 -1
  131. package/src/lib/hooks/useCanvasEvents.ts +0 -1
  132. package/src/lib/primitives/Box.ts +0 -8
  133. package/src/lib/utils/areShapesContentEqual.ts +1 -2
  134. package/src/lib/utils/reorderShapes.ts +10 -13
  135. package/src/lib/utils/richText.ts +10 -4
  136. package/src/version.ts +3 -3
  137. package/dist-cjs/lib/editor/managers/ClickManager.js.map +0 -7
  138. package/dist-cjs/lib/editor/managers/EdgeScrollManager.js.map +0 -7
  139. package/dist-cjs/lib/editor/managers/FocusManager.js.map +0 -7
  140. package/dist-cjs/lib/editor/managers/FontManager.js.map +0 -7
  141. package/dist-cjs/lib/editor/managers/HistoryManager.js.map +0 -7
  142. package/dist-cjs/lib/editor/managers/ScribbleManager.js.map +0 -7
  143. package/dist-cjs/lib/editor/managers/Stack.js +0 -82
  144. package/dist-cjs/lib/editor/managers/Stack.js.map +0 -7
  145. package/dist-cjs/lib/editor/managers/TextManager.js.map +0 -7
  146. package/dist-cjs/lib/editor/managers/TickManager.js.map +0 -7
  147. package/dist-cjs/lib/editor/managers/UserPreferencesManager.js.map +0 -7
  148. package/dist-esm/lib/editor/managers/ClickManager.mjs.map +0 -7
  149. package/dist-esm/lib/editor/managers/EdgeScrollManager.mjs.map +0 -7
  150. package/dist-esm/lib/editor/managers/FocusManager.mjs.map +0 -7
  151. package/dist-esm/lib/editor/managers/FontManager.mjs.map +0 -7
  152. package/dist-esm/lib/editor/managers/HistoryManager.mjs.map +0 -7
  153. package/dist-esm/lib/editor/managers/ScribbleManager.mjs.map +0 -7
  154. package/dist-esm/lib/editor/managers/Stack.mjs +0 -62
  155. package/dist-esm/lib/editor/managers/Stack.mjs.map +0 -7
  156. package/dist-esm/lib/editor/managers/TextManager.mjs.map +0 -7
  157. package/dist-esm/lib/editor/managers/TickManager.mjs.map +0 -7
  158. package/dist-esm/lib/editor/managers/UserPreferencesManager.mjs.map +0 -7
  159. package/src/lib/editor/managers/ScribbleManager.test.ts +0 -32
  160. package/src/lib/editor/managers/Stack.ts +0 -71
  161. /package/dist-cjs/lib/editor/managers/{FocusManager.js → FocusManager/FocusManager.js} +0 -0
  162. /package/dist-esm/lib/editor/managers/{FocusManager.mjs → FocusManager/FocusManager.mjs} +0 -0
@@ -0,0 +1,442 @@
1
+ import { Editor } from '../../Editor'
2
+ import { TLClickEventInfo, TLPointerEventInfo } from '../../types/event-types'
3
+ import { ClickManager } from './ClickManager'
4
+
5
+ // Mock the Editor class
6
+ jest.mock('../../Editor')
7
+
8
+ describe('ClickManager', () => {
9
+ let editor: jest.Mocked<Editor>
10
+ let clickManager: ClickManager
11
+ let mockTimers: any
12
+
13
+ const createPointerEvent = (
14
+ name: 'pointer_down' | 'pointer_up' | 'pointer_move',
15
+ point: { x: number; y: number } = { x: 0, y: 0 }
16
+ ): TLPointerEventInfo => ({
17
+ type: 'pointer',
18
+ name,
19
+ point,
20
+ pointerId: 1,
21
+ button: 0,
22
+ isPen: false,
23
+ target: 'canvas',
24
+ shiftKey: false,
25
+ altKey: false,
26
+ ctrlKey: false,
27
+ metaKey: false,
28
+ accelKey: false,
29
+ })
30
+
31
+ beforeEach(() => {
32
+ jest.useFakeTimers()
33
+ mockTimers = {
34
+ setTimeout: jest.fn((fn, delay) => setTimeout(fn, delay)),
35
+ }
36
+
37
+ editor = {
38
+ timers: mockTimers,
39
+ dispatch: jest.fn(),
40
+ options: {
41
+ doubleClickDurationMs: 300,
42
+ multiClickDurationMs: 300,
43
+ dragDistanceSquared: 16,
44
+ coarseDragDistanceSquared: 36,
45
+ },
46
+ inputs: {
47
+ currentScreenPoint: { x: 0, y: 0 },
48
+ },
49
+ getInstanceState: jest.fn(() => ({
50
+ isCoarsePointer: false,
51
+ })),
52
+ } as any
53
+
54
+ clickManager = new ClickManager(editor)
55
+ })
56
+
57
+ afterEach(() => {
58
+ jest.useRealTimers()
59
+ jest.clearAllMocks()
60
+ })
61
+
62
+ describe('constructor and initial state', () => {
63
+ it('should initialize with idle state', () => {
64
+ expect(clickManager.clickState).toBe('idle')
65
+ })
66
+
67
+ it('should store reference to editor', () => {
68
+ expect(clickManager.editor).toBe(editor)
69
+ })
70
+
71
+ it('should initialize lastPointerInfo as empty object', () => {
72
+ expect(clickManager.lastPointerInfo).toEqual({})
73
+ })
74
+ })
75
+
76
+ describe('single click behavior', () => {
77
+ it('should handle pointer_down in idle state', () => {
78
+ const pointerEvent = createPointerEvent('pointer_down', { x: 100, y: 100 })
79
+
80
+ const result = clickManager.handlePointerEvent(pointerEvent)
81
+
82
+ expect(result).toBe(pointerEvent)
83
+ expect(clickManager.clickState).toBe('pendingDouble')
84
+ expect(clickManager.lastPointerInfo).toBe(pointerEvent)
85
+ })
86
+
87
+ it('should handle pointer_up without generating click events in pending state', () => {
88
+ const downEvent = createPointerEvent('pointer_down', { x: 100, y: 100 })
89
+ const upEvent = createPointerEvent('pointer_up', { x: 100, y: 100 })
90
+
91
+ clickManager.handlePointerEvent(downEvent)
92
+ clickManager.handlePointerEvent(upEvent)
93
+
94
+ expect(clickManager.clickState).toBe('pendingDouble')
95
+ })
96
+
97
+ it('should return to idle state after timeout in pendingDouble', () => {
98
+ const pointerEvent = createPointerEvent('pointer_down', { x: 100, y: 100 })
99
+
100
+ clickManager.handlePointerEvent(pointerEvent)
101
+ expect(clickManager.clickState).toBe('pendingDouble')
102
+
103
+ jest.advanceTimersByTime(350)
104
+
105
+ expect(clickManager.clickState).toBe('idle')
106
+ })
107
+ })
108
+
109
+ describe('double click detection', () => {
110
+ it('should detect double click on second pointer_down', () => {
111
+ const firstDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
112
+ const secondDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
113
+
114
+ clickManager.handlePointerEvent(firstDown)
115
+ const result = clickManager.handlePointerEvent(secondDown) as TLClickEventInfo
116
+
117
+ expect(result.type).toBe('click')
118
+ expect(result.name).toBe('double_click')
119
+ expect(result.phase).toBe('down')
120
+ expect(clickManager.clickState).toBe('pendingTriple')
121
+ })
122
+
123
+ it('should generate double_click up event on pointer_up after double_click down', () => {
124
+ const firstDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
125
+ const secondDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
126
+ const secondUp = createPointerEvent('pointer_up', { x: 100, y: 100 })
127
+
128
+ clickManager.handlePointerEvent(firstDown)
129
+ clickManager.handlePointerEvent(secondDown)
130
+ const result = clickManager.handlePointerEvent(secondUp) as TLClickEventInfo
131
+
132
+ expect(result.type).toBe('click')
133
+ expect(result.name).toBe('double_click')
134
+ expect(result.phase).toBe('up')
135
+ })
136
+
137
+ it('should dispatch double_click settle event after timeout in pendingTriple', () => {
138
+ const firstDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
139
+ const secondDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
140
+
141
+ clickManager.handlePointerEvent(firstDown)
142
+ clickManager.handlePointerEvent(secondDown)
143
+
144
+ jest.advanceTimersByTime(350)
145
+
146
+ expect(editor.dispatch).toHaveBeenCalledWith(
147
+ expect.objectContaining({
148
+ type: 'click',
149
+ name: 'double_click',
150
+ phase: 'settle',
151
+ })
152
+ )
153
+ expect(clickManager.clickState).toBe('idle')
154
+ })
155
+ })
156
+
157
+ describe('triple and quadruple click detection', () => {
158
+ it('should detect triple click on third pointer_down', () => {
159
+ const firstDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
160
+ const secondDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
161
+ const thirdDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
162
+
163
+ clickManager.handlePointerEvent(firstDown)
164
+ clickManager.handlePointerEvent(secondDown)
165
+ const result = clickManager.handlePointerEvent(thirdDown) as TLClickEventInfo
166
+
167
+ expect(result.type).toBe('click')
168
+ expect(result.name).toBe('triple_click')
169
+ expect(result.phase).toBe('down')
170
+ expect(clickManager.clickState).toBe('pendingQuadruple')
171
+ })
172
+
173
+ it('should detect quadruple click on fourth pointer_down', () => {
174
+ const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
175
+
176
+ clickManager.handlePointerEvent(pointerDown) // first
177
+ clickManager.handlePointerEvent(pointerDown) // second (double_click)
178
+ clickManager.handlePointerEvent(pointerDown) // third (triple_click)
179
+ const result = clickManager.handlePointerEvent(pointerDown) as TLClickEventInfo // fourth
180
+
181
+ expect(result.type).toBe('click')
182
+ expect(result.name).toBe('quadruple_click')
183
+ expect(result.phase).toBe('down')
184
+ expect(clickManager.clickState).toBe('pendingOverflow')
185
+ })
186
+
187
+ it('should handle overflow state after quadruple click', () => {
188
+ const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
189
+
190
+ clickManager.handlePointerEvent(pointerDown) // first
191
+ clickManager.handlePointerEvent(pointerDown) // second
192
+ clickManager.handlePointerEvent(pointerDown) // third
193
+ clickManager.handlePointerEvent(pointerDown) // fourth
194
+ const result = clickManager.handlePointerEvent(pointerDown) // fifth
195
+
196
+ expect(result).toBe(pointerDown)
197
+ expect(clickManager.clickState).toBe('overflow')
198
+ })
199
+
200
+ it('should generate triple_click up event on pointer_up after triple_click down', () => {
201
+ const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
202
+ const pointerUp = createPointerEvent('pointer_up', { x: 100, y: 100 })
203
+
204
+ clickManager.handlePointerEvent(pointerDown) // first
205
+ clickManager.handlePointerEvent(pointerDown) // second
206
+ clickManager.handlePointerEvent(pointerDown) // third
207
+ const result = clickManager.handlePointerEvent(pointerUp) as TLClickEventInfo
208
+
209
+ expect(result.type).toBe('click')
210
+ expect(result.name).toBe('triple_click')
211
+ expect(result.phase).toBe('up')
212
+ })
213
+
214
+ it('should generate quadruple_click up event on pointer_up after quadruple_click down', () => {
215
+ const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
216
+ const pointerUp = createPointerEvent('pointer_up', { x: 100, y: 100 })
217
+
218
+ clickManager.handlePointerEvent(pointerDown) // first
219
+ clickManager.handlePointerEvent(pointerDown) // second
220
+ clickManager.handlePointerEvent(pointerDown) // third
221
+ clickManager.handlePointerEvent(pointerDown) // fourth
222
+ const result = clickManager.handlePointerEvent(pointerUp) as TLClickEventInfo
223
+
224
+ expect(result.type).toBe('click')
225
+ expect(result.name).toBe('quadruple_click')
226
+ expect(result.phase).toBe('up')
227
+ })
228
+ })
229
+
230
+ describe('timeout behavior and settle events', () => {
231
+ it('should dispatch triple_click settle event after timeout in pendingQuadruple', () => {
232
+ const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
233
+
234
+ clickManager.handlePointerEvent(pointerDown) // first
235
+ clickManager.handlePointerEvent(pointerDown) // second
236
+ clickManager.handlePointerEvent(pointerDown) // third
237
+
238
+ jest.advanceTimersByTime(350)
239
+
240
+ expect(editor.dispatch).toHaveBeenCalledWith(
241
+ expect.objectContaining({
242
+ type: 'click',
243
+ name: 'triple_click',
244
+ phase: 'settle',
245
+ })
246
+ )
247
+ expect(clickManager.clickState).toBe('idle')
248
+ })
249
+
250
+ it('should dispatch quadruple_click settle event after timeout in pendingOverflow', () => {
251
+ const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
252
+
253
+ clickManager.handlePointerEvent(pointerDown) // first
254
+ clickManager.handlePointerEvent(pointerDown) // second
255
+ clickManager.handlePointerEvent(pointerDown) // third
256
+ clickManager.handlePointerEvent(pointerDown) // fourth
257
+
258
+ jest.advanceTimersByTime(350)
259
+
260
+ expect(editor.dispatch).toHaveBeenCalledWith(
261
+ expect.objectContaining({
262
+ type: 'click',
263
+ name: 'quadruple_click',
264
+ phase: 'settle',
265
+ })
266
+ )
267
+ expect(clickManager.clickState).toBe('idle')
268
+ })
269
+
270
+ it('should use different timeout durations for different states', () => {
271
+ const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
272
+
273
+ // First click - should use doubleClickDurationMs
274
+ clickManager.handlePointerEvent(pointerDown)
275
+ expect(mockTimers.setTimeout).toHaveBeenCalledWith(
276
+ expect.any(Function),
277
+ editor.options.doubleClickDurationMs
278
+ )
279
+
280
+ jest.clearAllMocks()
281
+
282
+ // Second click - should use multiClickDurationMs
283
+ clickManager.handlePointerEvent(pointerDown)
284
+ expect(mockTimers.setTimeout).toHaveBeenCalledWith(
285
+ expect.any(Function),
286
+ editor.options.multiClickDurationMs
287
+ )
288
+ })
289
+ })
290
+
291
+ describe('distance-based click cancellation', () => {
292
+ it('should reset to idle if clicks are too far apart', () => {
293
+ const firstDown = createPointerEvent('pointer_down', { x: 0, y: 0 })
294
+ const secondDown = createPointerEvent('pointer_down', { x: 50, y: 50 }) // > 40px distance
295
+
296
+ clickManager.handlePointerEvent(firstDown)
297
+ expect(clickManager.clickState).toBe('pendingDouble')
298
+
299
+ const result = clickManager.handlePointerEvent(secondDown)
300
+
301
+ expect(result).toBe(secondDown)
302
+ expect(clickManager.clickState).toBe('pendingDouble') // Reset and started new sequence
303
+ })
304
+
305
+ it('should continue sequence if clicks are close enough', () => {
306
+ const firstDown = createPointerEvent('pointer_down', { x: 0, y: 0 })
307
+ const secondDown = createPointerEvent('pointer_down', { x: 5, y: 5 }) // < 40px distance
308
+
309
+ clickManager.handlePointerEvent(firstDown)
310
+ const result = clickManager.handlePointerEvent(secondDown) as TLClickEventInfo
311
+
312
+ expect(result.type).toBe('click')
313
+ expect(result.name).toBe('double_click')
314
+ expect(clickManager.clickState).toBe('pendingTriple')
315
+ })
316
+ })
317
+
318
+ describe('pointer move cancellation behavior', () => {
319
+ it('should cancel click sequence on significant pointer move', () => {
320
+ const downEvent = createPointerEvent('pointer_down', { x: 0, y: 0 })
321
+ const moveEvent = createPointerEvent('pointer_move', { x: 10, y: 10 })
322
+
323
+ editor.inputs.currentScreenPoint.x = 10
324
+ editor.inputs.currentScreenPoint.y = 10
325
+
326
+ clickManager.handlePointerEvent(downEvent)
327
+ expect(clickManager.clickState).toBe('pendingDouble')
328
+
329
+ const result = clickManager.handlePointerEvent(moveEvent)
330
+
331
+ expect(result).toBe(moveEvent)
332
+ expect(clickManager.clickState).toBe('idle')
333
+ })
334
+
335
+ it('should use coarse drag distance for coarse pointers', () => {
336
+ editor.getInstanceState.mockReturnValue({
337
+ ...editor.getInstanceState(),
338
+ isCoarsePointer: true,
339
+ })
340
+
341
+ const downEvent = createPointerEvent('pointer_down', { x: 0, y: 0 })
342
+ const moveEvent1 = createPointerEvent('pointer_move', { x: 1, y: 1 })
343
+ const moveEvent2 = createPointerEvent('pointer_move', { x: 5, y: 5 }) // 50
344
+
345
+ clickManager.handlePointerEvent(downEvent)
346
+ expect(clickManager.clickState).toBe('pendingDouble')
347
+
348
+ // Should not cancel for coarse pointer with small movement
349
+ editor.inputs.currentScreenPoint.x = 1
350
+ editor.inputs.currentScreenPoint.y = 1
351
+ clickManager.handlePointerEvent(moveEvent1)
352
+ expect(clickManager.clickState).toBe('pendingDouble')
353
+
354
+ editor.inputs.currentScreenPoint.x = 5
355
+ editor.inputs.currentScreenPoint.y = 5
356
+ clickManager.handlePointerEvent(moveEvent2)
357
+
358
+ expect(clickManager.clickState).toBe('idle')
359
+ })
360
+
361
+ it('should not cancel in idle state', () => {
362
+ const moveEvent = createPointerEvent('pointer_move', { x: 100, y: 100 })
363
+
364
+ editor.inputs.currentScreenPoint.x = 100
365
+ editor.inputs.currentScreenPoint.y = 100
366
+
367
+ clickManager.handlePointerEvent(moveEvent)
368
+
369
+ expect(clickManager.clickState).toBe('idle')
370
+ })
371
+ })
372
+
373
+ describe('cancelDoubleClickTimeout method', () => {
374
+ it('should clear timeout and reset state to idle', () => {
375
+ const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
376
+
377
+ clickManager.handlePointerEvent(pointerDown)
378
+ expect(clickManager.clickState).toBe('pendingDouble')
379
+
380
+ clickManager.cancelDoubleClickTimeout()
381
+
382
+ expect(clickManager.clickState).toBe('idle')
383
+ })
384
+
385
+ it('should prevent timeout callback from executing after cancellation', () => {
386
+ const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
387
+
388
+ clickManager.handlePointerEvent(pointerDown)
389
+ clickManager.handlePointerEvent(pointerDown) // double click
390
+ expect(clickManager.clickState).toBe('pendingTriple')
391
+
392
+ clickManager.cancelDoubleClickTimeout()
393
+
394
+ // Advance time - should not dispatch settle event
395
+ jest.advanceTimersByTime(350)
396
+
397
+ expect(editor.dispatch).not.toHaveBeenCalled()
398
+ expect(clickManager.clickState).toBe('idle')
399
+ })
400
+ })
401
+
402
+ describe('edge cases', () => {
403
+ it('should handle null click state gracefully', () => {
404
+ // Force null state
405
+ ;(clickManager as any)._clickState = null
406
+
407
+ const pointerEvent = createPointerEvent('pointer_down', { x: 100, y: 100 })
408
+ const result = clickManager.handlePointerEvent(pointerEvent)
409
+
410
+ expect(result).toBe(pointerEvent)
411
+ })
412
+
413
+ it('should handle missing previous screen point', () => {
414
+ const firstDown = createPointerEvent('pointer_down', { x: 0, y: 0 })
415
+
416
+ // Clear previous point
417
+ ;(clickManager as any)._previousScreenPoint = undefined
418
+
419
+ const result = clickManager.handlePointerEvent(firstDown)
420
+
421
+ expect(result).toBe(firstDown)
422
+ expect(clickManager.clickState).toBe('pendingDouble')
423
+ })
424
+
425
+ it('should handle overflow state correctly', () => {
426
+ const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
427
+ const pointerUp = createPointerEvent('pointer_up', { x: 100, y: 100 })
428
+
429
+ // Get to overflow state
430
+ clickManager.handlePointerEvent(pointerDown) // 1
431
+ clickManager.handlePointerEvent(pointerDown) // 2
432
+ clickManager.handlePointerEvent(pointerDown) // 3
433
+ clickManager.handlePointerEvent(pointerDown) // 4
434
+ clickManager.handlePointerEvent(pointerDown) // 5 -> overflow
435
+
436
+ expect(clickManager.clickState).toBe('overflow')
437
+
438
+ // pointer_up in overflow should just return the event
439
+ clickManager.handlePointerEvent(pointerUp)
440
+ })
441
+ })
442
+ })
@@ -1,7 +1,7 @@
1
1
  import { bind, uniqueId } from '@tldraw/utils'
2
- import { Vec } from '../../primitives/Vec'
3
- import type { Editor } from '../Editor'
4
- import { TLClickEventInfo, TLPointerEventInfo } from '../types/event-types'
2
+ import { Vec } from '../../../primitives/Vec'
3
+ import type { Editor } from '../../Editor'
4
+ import { TLClickEventInfo, TLPointerEventInfo } from '../../types/event-types'
5
5
 
6
6
  /** @public */
7
7
  export type TLClickState =