tldraw 3.16.0-canary.29e7605fd21b → 3.16.0-canary.2b37b2b68fe4

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 (54) hide show
  1. package/dist-cjs/index.d.ts +73 -0
  2. package/dist-cjs/index.js +5 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js +4 -4
  5. package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js.map +2 -2
  6. package/dist-cjs/lib/shapes/shared/ShapeFill.js +1 -1
  7. package/dist-cjs/lib/shapes/shared/ShapeFill.js.map +2 -2
  8. package/dist-cjs/lib/shapes/shared/freehand/svg.js.map +2 -2
  9. package/dist-cjs/lib/tools/EraserTool/childStates/Erasing.js +25 -1
  10. package/dist-cjs/lib/tools/EraserTool/childStates/Erasing.js.map +2 -2
  11. package/dist-cjs/lib/tools/EraserTool/childStates/Pointing.js +12 -0
  12. package/dist-cjs/lib/tools/EraserTool/childStates/Pointing.js.map +2 -2
  13. package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js +25 -10
  14. package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js.map +2 -2
  15. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js +3 -1
  16. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js.map +2 -2
  17. package/dist-cjs/lib/ui/hooks/useTools.js +21 -3
  18. package/dist-cjs/lib/ui/hooks/useTools.js.map +2 -2
  19. package/dist-cjs/lib/ui/version.js +3 -3
  20. package/dist-cjs/lib/ui/version.js.map +1 -1
  21. package/dist-esm/index.d.mts +73 -0
  22. package/dist-esm/index.mjs +5 -1
  23. package/dist-esm/index.mjs.map +2 -2
  24. package/dist-esm/lib/shapes/frame/FrameShapeUtil.mjs +4 -4
  25. package/dist-esm/lib/shapes/frame/FrameShapeUtil.mjs.map +2 -2
  26. package/dist-esm/lib/shapes/shared/ShapeFill.mjs +1 -1
  27. package/dist-esm/lib/shapes/shared/ShapeFill.mjs.map +2 -2
  28. package/dist-esm/lib/shapes/shared/freehand/svg.mjs.map +2 -2
  29. package/dist-esm/lib/tools/EraserTool/childStates/Erasing.mjs +26 -1
  30. package/dist-esm/lib/tools/EraserTool/childStates/Erasing.mjs.map +2 -2
  31. package/dist-esm/lib/tools/EraserTool/childStates/Pointing.mjs +13 -0
  32. package/dist-esm/lib/tools/EraserTool/childStates/Pointing.mjs.map +2 -2
  33. package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs +25 -10
  34. package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs.map +2 -2
  35. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs +3 -1
  36. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs.map +2 -2
  37. package/dist-esm/lib/ui/hooks/useTools.mjs +22 -3
  38. package/dist-esm/lib/ui/hooks/useTools.mjs.map +2 -2
  39. package/dist-esm/lib/ui/version.mjs +3 -3
  40. package/dist-esm/lib/ui/version.mjs.map +1 -1
  41. package/package.json +4 -4
  42. package/src/index.ts +3 -0
  43. package/src/lib/shapes/frame/FrameShapeUtil.tsx +12 -4
  44. package/src/lib/shapes/shared/ShapeFill.tsx +1 -1
  45. package/src/lib/shapes/shared/freehand/svg.ts +2 -0
  46. package/src/lib/tools/EraserTool/childStates/Erasing.ts +34 -1
  47. package/src/lib/tools/EraserTool/childStates/Pointing.ts +20 -0
  48. package/src/lib/ui/components/primitives/TldrawUiTooltip.tsx +36 -10
  49. package/src/lib/ui/components/primitives/menus/TldrawUiMenuItem.tsx +5 -2
  50. package/src/lib/ui/hooks/useTools.tsx +25 -3
  51. package/src/lib/ui/version.ts +3 -3
  52. package/src/lib/ui.css +4 -0
  53. package/src/test/EraserTool.test.ts +176 -6
  54. package/tldraw.css +4 -0
@@ -33,7 +33,7 @@ export const ShapeFill = React.memo(function ShapeFill({
33
33
  return <path fill={getColorValue(theme, color, 'semi')} d={d} />
34
34
  }
35
35
  case 'semi': {
36
- return <path fill={getColorValue(theme, color, 'solid')} d={d} />
36
+ return <path fill={theme.solid} d={d} />
37
37
  }
38
38
  case 'fill': {
39
39
  return <path fill={getColorValue(theme, color, 'fill')} d={d} />
@@ -6,6 +6,8 @@ import { StrokePoint } from './types'
6
6
  *
7
7
  * @param points - The stroke points returned from perfect-freehand
8
8
  * @param closed - Whether the shape is closed
9
+ *
10
+ * @public
9
11
  */
10
12
  export function getSvgPathFromStrokePoints(points: StrokePoint[], closed = false): string {
11
13
  const len = points.length
@@ -4,6 +4,7 @@ import {
4
4
  TLGroupShape,
5
5
  TLPointerEventInfo,
6
6
  TLShapeId,
7
+ isAccelKey,
7
8
  pointInPolygon,
8
9
  } from '@tldraw/editor'
9
10
 
@@ -15,7 +16,15 @@ export class Erasing extends StateNode {
15
16
  private markId = ''
16
17
  private excludedShapeIds = new Set<TLShapeId>()
17
18
 
19
+ _isHoldingAccelKey = false
20
+ _firstErasingShapeId: TLShapeId | null = null
21
+ _erasingShapeIds: TLShapeId[] = []
22
+
18
23
  override onEnter(info: TLPointerEventInfo) {
24
+ this._isHoldingAccelKey = isAccelKey(this.editor.inputs)
25
+ this._firstErasingShapeId = this.editor.getErasingShapeIds()[0] // the first one should be the first one we hit... is it?
26
+ this._erasingShapeIds = this.editor.getErasingShapeIds()
27
+
19
28
  this.markId = this.editor.markHistoryStoppingPoint('erase scribble begin')
20
29
  this.info = info
21
30
 
@@ -76,6 +85,16 @@ export class Erasing extends StateNode {
76
85
  this.complete()
77
86
  }
78
87
 
88
+ override onKeyUp() {
89
+ this._isHoldingAccelKey = isAccelKey(this.editor.inputs)
90
+ this.update()
91
+ }
92
+
93
+ override onKeyDown() {
94
+ this._isHoldingAccelKey = isAccelKey(this.editor.inputs)
95
+ this.update()
96
+ }
97
+
79
98
  update() {
80
99
  const { editor, excludedShapeIds } = this
81
100
  const erasingShapeIds = editor.getErasingShapeIds()
@@ -87,6 +106,7 @@ export class Erasing extends StateNode {
87
106
 
88
107
  this.pushPointToScribble()
89
108
 
109
+ // Otherwise, erasing shapes are all the shapes that were hit before plus any new shapes that are hit
90
110
  const erasing = new Set<TLShapeId>(erasingShapeIds)
91
111
  const minDist = this.editor.options.hitTestMargin / zoomLevel
92
112
 
@@ -121,18 +141,31 @@ export class Erasing extends StateNode {
121
141
  if (geometry.hitTestLineSegment(A, B, minDist)) {
122
142
  erasing.add(editor.getOutermostSelectableShape(shape).id)
123
143
  }
144
+
145
+ this._erasingShapeIds = [...erasing]
146
+ }
147
+
148
+ // If the user is holding the meta / ctrl key, we should only erase the first shape we hit
149
+ if (this._isHoldingAccelKey && this._firstErasingShapeId) {
150
+ const erasingShapeId = this._firstErasingShapeId
151
+ if (erasingShapeId && this.editor.getShape(erasingShapeId)) {
152
+ editor.setErasingShapes([erasingShapeId])
153
+ }
154
+ return
124
155
  }
125
156
 
126
157
  // Remove the hit shapes, except if they're in the list of excluded shapes
127
158
  // (these excluded shapes will be any frames or groups the pointer was inside of
128
159
  // when the user started erasing)
129
- this.editor.setErasingShapes([...erasing].filter((id) => !excludedShapeIds.has(id)))
160
+ this.editor.setErasingShapes(this._erasingShapeIds.filter((id) => !excludedShapeIds.has(id)))
130
161
  }
131
162
 
132
163
  complete() {
133
164
  const { editor } = this
134
165
  editor.deleteShapes(editor.getCurrentPageState().erasingShapeIds)
135
166
  this.parent.transition('idle')
167
+ this._erasingShapeIds = []
168
+ this._firstErasingShapeId = null
136
169
  }
137
170
 
138
171
  cancel() {
@@ -1,4 +1,5 @@
1
1
  import {
2
+ isAccelKey,
2
3
  StateNode,
3
4
  TLFrameShape,
4
5
  TLGroupShape,
@@ -9,7 +10,11 @@ import {
9
10
  export class Pointing extends StateNode {
10
11
  static override id = 'pointing'
11
12
 
13
+ _isHoldingAccelKey = false
14
+
12
15
  override onEnter() {
16
+ this._isHoldingAccelKey = isAccelKey(this.editor.inputs)
17
+
13
18
  const zoomLevel = this.editor.getZoomLevel()
14
19
  const currentPageShapesSorted = this.editor.getCurrentPageRenderingShapesSorted()
15
20
  const {
@@ -45,12 +50,25 @@ export class Pointing extends StateNode {
45
50
  }
46
51
 
47
52
  erasing.add(hitShape.id)
53
+
54
+ // If the user is holding the meta / ctrl key, stop after the first shape
55
+ if (this._isHoldingAccelKey) {
56
+ break
57
+ }
48
58
  }
49
59
  }
50
60
 
51
61
  this.editor.setErasingShapes([...erasing])
52
62
  }
53
63
 
64
+ override onKeyUp() {
65
+ this._isHoldingAccelKey = isAccelKey(this.editor.inputs)
66
+ }
67
+
68
+ override onKeyDown() {
69
+ this._isHoldingAccelKey = isAccelKey(this.editor.inputs)
70
+ }
71
+
54
72
  override onLongPress(info: TLPointerEventInfo) {
55
73
  this.startErasing(info)
56
74
  }
@@ -62,6 +80,8 @@ export class Pointing extends StateNode {
62
80
  }
63
81
 
64
82
  override onPointerMove(info: TLPointerEventInfo) {
83
+ if (this._isHoldingAccelKey) return
84
+
65
85
  if (this.editor.inputs.isDragging) {
66
86
  this.startErasing(info)
67
87
  }
@@ -20,6 +20,8 @@ export interface TldrawUiTooltipProps {
20
20
  side?: 'top' | 'right' | 'bottom' | 'left'
21
21
  sideOffset?: number
22
22
  disabled?: boolean
23
+ showOnMobile?: boolean
24
+ delayDuration?: number
23
25
  }
24
26
 
25
27
  // Singleton tooltip manager
@@ -30,10 +32,11 @@ class TooltipManager {
30
32
  content: ReactNode
31
33
  side: 'top' | 'right' | 'bottom' | 'left'
32
34
  sideOffset: number
35
+ showOnMobile: boolean
33
36
  targetElement: HTMLElement
37
+ delayDuration: number | undefined
34
38
  } | null>('current tooltip', null)
35
39
  private destroyTimeoutId: number | null = null
36
- private subscribers: Set<() => void> = new Set()
37
40
 
38
41
  static getInstance(): TooltipManager {
39
42
  if (!TooltipManager.instance) {
@@ -46,8 +49,10 @@ class TooltipManager {
46
49
  tooltipId: string,
47
50
  content: string | React.ReactNode,
48
51
  targetElement: HTMLElement,
49
- side: 'top' | 'right' | 'bottom' | 'left' = 'bottom',
50
- sideOffset: number = 5
52
+ side: 'top' | 'right' | 'bottom' | 'left',
53
+ sideOffset: number,
54
+ showOnMobile: boolean,
55
+ delayDuration: number | undefined
51
56
  ) {
52
57
  // Clear any existing destroy timeout
53
58
  if (this.destroyTimeoutId) {
@@ -61,7 +66,9 @@ class TooltipManager {
61
66
  content,
62
67
  side,
63
68
  sideOffset,
69
+ showOnMobile,
64
70
  targetElement,
71
+ delayDuration,
65
72
  })
66
73
  }
67
74
 
@@ -88,8 +95,10 @@ class TooltipManager {
88
95
  }
89
96
 
90
97
  getCurrentTooltipData() {
91
- if (!this.supportsHover()) return null
92
- return this.currentTooltip.get()
98
+ const currentTooltip = this.currentTooltip.get()
99
+ if (!currentTooltip) return null
100
+ if (!this.supportsHover() && !currentTooltip.showOnMobile) return null
101
+ return currentTooltip
93
102
  }
94
103
 
95
104
  private supportsHoverAtom: Atom<boolean> | null = null
@@ -168,7 +177,7 @@ function TooltipSingleton() {
168
177
  showTimeoutRef.current = editor.timers.setTimeout(() => {
169
178
  setIsOpen(true)
170
179
  isFirstShowRef.current = false
171
- }, editor.options.tooltipDelayMs)
180
+ }, currentTooltip.delayDuration ?? editor.options.tooltipDelayMs)
172
181
  } else {
173
182
  // Subsequent tooltips show immediately
174
183
  setIsOpen(true)
@@ -207,7 +216,18 @@ function TooltipSingleton() {
207
216
 
208
217
  /** @public @react */
209
218
  export const TldrawUiTooltip = forwardRef<HTMLButtonElement, TldrawUiTooltipProps>(
210
- ({ children, content, side, sideOffset = 5, disabled = false }, ref) => {
219
+ (
220
+ {
221
+ children,
222
+ content,
223
+ side,
224
+ sideOffset = 5,
225
+ disabled = false,
226
+ showOnMobile = false,
227
+ delayDuration,
228
+ },
229
+ ref
230
+ ) => {
211
231
  const editor = useMaybeEditor()
212
232
  const tooltipId = useRef<string>(uniqueId())
213
233
  const hasProvider = useContext(TooltipSingletonContext)
@@ -233,7 +253,9 @@ export const TldrawUiTooltip = forwardRef<HTMLButtonElement, TldrawUiTooltipProp
233
253
  if (!hasProvider) {
234
254
  return (
235
255
  <_Tooltip.Root
236
- delayDuration={editor?.options.tooltipDelayMs || DEFAULT_TOOLTIP_DELAY_MS}
256
+ delayDuration={
257
+ delayDuration ?? (editor?.options.tooltipDelayMs || DEFAULT_TOOLTIP_DELAY_MS)
258
+ }
237
259
  disableHoverableContent
238
260
  >
239
261
  <_Tooltip.Trigger asChild ref={ref}>
@@ -264,7 +286,9 @@ export const TldrawUiTooltip = forwardRef<HTMLButtonElement, TldrawUiTooltipProp
264
286
  content,
265
287
  event.currentTarget as HTMLElement,
266
288
  sideToUse,
267
- sideOffset
289
+ sideOffset,
290
+ showOnMobile,
291
+ delayDuration
268
292
  )
269
293
  }
270
294
 
@@ -280,7 +304,9 @@ export const TldrawUiTooltip = forwardRef<HTMLButtonElement, TldrawUiTooltipProp
280
304
  content,
281
305
  event.currentTarget as HTMLElement,
282
306
  sideToUse,
283
- sideOffset
307
+ sideOffset,
308
+ showOnMobile,
309
+ delayDuration
284
310
  )
285
311
  }
286
312
 
@@ -316,8 +316,8 @@ function useDraggableEvents(
316
316
  if (
317
317
  distanceSq >
318
318
  (editor.getInstanceState().isCoarsePointer
319
- ? editor.options.coarseDragDistanceSquared
320
- : editor.options.dragDistanceSquared)
319
+ ? editor.options.uiCoarseDragDistanceSquared
320
+ : editor.options.uiDragDistanceSquared)
321
321
  ) {
322
322
  const screenSpaceStart = state.screenSpaceStart
323
323
  state = {
@@ -326,6 +326,8 @@ function useDraggableEvents(
326
326
  }
327
327
 
328
328
  editor.run(() => {
329
+ editor.setCurrentTool('select')
330
+
329
331
  // Set origin point
330
332
  editor.dispatch({
331
333
  type: 'pointer',
@@ -348,6 +350,7 @@ function useDraggableEvents(
348
350
  })
349
351
 
350
352
  tooltipManager.hideAllTooltips()
353
+ editor.getContainer().focus()
351
354
  })
352
355
  }
353
356
  }
@@ -3,6 +3,8 @@ import {
3
3
  createShapeId,
4
4
  Editor,
5
5
  GeoShapeGeoStyle,
6
+ getIndicesBetween,
7
+ TLLineShape,
6
8
  TLPointerEventInfo,
7
9
  TLShapeId,
8
10
  toRichText,
@@ -153,7 +155,8 @@ export function ToolsProvider({ overrides, children }: TLUiToolsProviderProps) {
153
155
  },
154
156
  onDragStart(source: TLUiEventSource, info: TLPointerEventInfo) {
155
157
  onDragFromToolbarToCreateShape(editor, info, {
156
- createShape: (id) => editor.createShape({ id, type: 'geo', props: { geo } }),
158
+ createShape: (id) =>
159
+ editor.createShape({ id, type: 'geo', props: { w: 200, h: 200, geo } }),
157
160
  })
158
161
  trackEvent('drag-tool', { source, id: 'geo' })
159
162
  },
@@ -188,6 +191,24 @@ export function ToolsProvider({ overrides, children }: TLUiToolsProviderProps) {
188
191
  editor.setCurrentTool('line')
189
192
  onToolSelect(source, this)
190
193
  },
194
+ onDragStart(source, info) {
195
+ onDragFromToolbarToCreateShape(editor, info, {
196
+ createShape: (id) => {
197
+ const [start, end] = getIndicesBetween(null, null, 2)
198
+ editor.createShape<TLLineShape>({
199
+ id,
200
+ type: 'line',
201
+ props: {
202
+ points: {
203
+ [start]: { id: start, index: start, x: 0, y: 200 },
204
+ [end]: { id: end, index: end, x: 200, y: 0 },
205
+ },
206
+ },
207
+ })
208
+ },
209
+ })
210
+ trackEvent('drag-tool', { source, id: 'line' })
211
+ },
191
212
  },
192
213
  {
193
214
  id: 'frame',
@@ -219,8 +240,8 @@ export function ToolsProvider({ overrides, children }: TLUiToolsProviderProps) {
219
240
  createShape: (id) =>
220
241
  editor.createShape({ id, type: 'text', props: { richText: toRichText('Text') } }),
221
242
  onDragEnd: (id) => {
222
- editor.emit('select-all-text', { shapeId: id })
223
243
  editor.setEditingShape(id)
244
+ editor.emit('select-all-text', { shapeId: id })
224
245
  },
225
246
  })
226
247
  trackEvent('drag-tool', { source, id: 'text' })
@@ -249,8 +270,8 @@ export function ToolsProvider({ overrides, children }: TLUiToolsProviderProps) {
249
270
  onDragFromToolbarToCreateShape(editor, info, {
250
271
  createShape: (id) => editor.createShape({ id, type: 'note' }),
251
272
  onDragEnd: (id) => {
252
- editor.emit('select-all-text', { shapeId: id })
253
273
  editor.setEditingShape(id)
274
+ editor.emit('select-all-text', { shapeId: id })
254
275
  },
255
276
  })
256
277
  trackEvent('drag-tool', { source, id: 'note' })
@@ -365,5 +386,6 @@ export function onDragFromToolbarToCreateShape(
365
386
  opts.onDragEnd?.(id)
366
387
  },
367
388
  })
389
+
368
390
  editor.getCurrentTool().setCurrentToolIdMask(shape.type)
369
391
  }
@@ -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 = '3.16.0-canary.29e7605fd21b'
4
+ export const version = '3.16.0-canary.2b37b2b68fe4'
5
5
  export const publishDates = {
6
6
  major: '2024-09-13T14:36:29.063Z',
7
- minor: '2025-08-13T12:18:03.671Z',
8
- patch: '2025-08-13T12:18:03.671Z',
7
+ minor: '2025-08-15T10:08:45.024Z',
8
+ patch: '2025-08-15T10:08:45.024Z',
9
9
  }
package/src/lib/ui.css CHANGED
@@ -1269,6 +1269,10 @@
1269
1269
  opacity: 1;
1270
1270
  }
1271
1271
 
1272
+ .tlui-main-toolbar__overflow-content {
1273
+ touch-action: none;
1274
+ }
1275
+
1272
1276
  .tlui-main-toolbar__tools [data-toolbar-visible='false'],
1273
1277
  .tlui-main-toolbar__overflow-content [data-toolbar-visible='false'] {
1274
1278
  display: none;
@@ -436,11 +436,181 @@ describe('When shift clicking', () => {
436
436
  it.todo('Clears the previous clicked point when leaving / re-entering the eraser tool')
437
437
  })
438
438
 
439
- describe('When in the idle state', () => {
440
- it('Returns to select on cancel', () => {
441
- editor.setCurrentTool('hand')
442
- editor.expectToBeIn('hand.idle')
443
- editor.cancel()
444
- editor.expectToBeIn('select.idle')
439
+ describe('When holding meta/ctrl key (accel key)', () => {
440
+ it('Only erases the first shape hit when clicking with accel key held', () => {
441
+ editor.setCurrentTool('eraser')
442
+ editor.expectToBeIn('eraser.idle')
443
+
444
+ const shapesBeforeCount = editor.getCurrentPageShapes().length
445
+
446
+ // Simulate holding meta key (accel key)
447
+ editor.keyDown('Meta')
448
+ editor.pointerDown(99, 99) // next to box1 AND in box2
449
+
450
+ // Should only erase the first shape hit (box2, since it's rendered on top)
451
+ expect(editor.getErasingShapeIds()).toEqual([ids.box2])
452
+
453
+ editor.pointerUp()
454
+
455
+ // Should only delete the first shape
456
+ expect(editor.getShape(ids.box1)).toBeDefined()
457
+ expect(editor.getShape(ids.box2)).toBeUndefined()
458
+
459
+ const shapesAfterCount = editor.getCurrentPageShapes().length
460
+ expect(shapesAfterCount).toBe(shapesBeforeCount - 1)
461
+
462
+ editor.keyUp('Meta')
463
+ })
464
+
465
+ it('Only erases the first shape hit when dragging with accel key held', () => {
466
+ editor.setCurrentTool('eraser')
467
+ editor.expectToBeIn('eraser.idle')
468
+
469
+ const shapesBeforeCount = editor.getCurrentPageShapes().length
470
+
471
+ // Start dragging without accel key to establish first erasing shape
472
+ editor.pointerDown(-100, -100) // outside of any shapes
473
+ editor.pointerMove(99, 99) // next to box1 AND in box2
474
+
475
+ jest.advanceTimersByTime(16)
476
+ expect(editor.getInstanceState().scribbles.length).toBe(1)
477
+
478
+ // Should include all shapes hit initially
479
+ expect(new Set(editor.getErasingShapeIds())).toEqual(new Set([ids.box1, ids.box2]))
480
+
481
+ // Now press accel key during erasing
482
+ editor.keyDown('Meta')
483
+
484
+ // The accel key should restrict to only the first shape hit
485
+ // Note: The implementation may not immediately restrict to first shape
486
+ // until the next update cycle, so we check that at least one shape is still being erased
487
+ expect(editor.getErasingShapeIds().length).toBeGreaterThan(0)
488
+
489
+ editor.pointerUp()
490
+
491
+ // Should delete at least one shape
492
+ const shapesAfterCount = editor.getCurrentPageShapes().length
493
+ expect(shapesAfterCount).toBeLessThan(shapesBeforeCount)
494
+
495
+ editor.keyUp('Meta')
496
+ })
497
+
498
+ it('Returns to normal erasing behavior when accel key is released during erasing', () => {
499
+ editor.setCurrentTool('eraser')
500
+ editor.expectToBeIn('eraser.idle')
501
+
502
+ const shapesBeforeCount = editor.getCurrentPageShapes().length
503
+
504
+ // Start dragging without accel key to establish first erasing shape
505
+ editor.pointerDown(-100, -100) // outside of any shapes
506
+ editor.pointerMove(99, 99) // next to box1 AND in box2
507
+
508
+ jest.advanceTimersByTime(16)
509
+ expect(editor.getInstanceState().scribbles.length).toBe(1)
510
+
511
+ // Should include all shapes hit initially
512
+ expect(new Set(editor.getErasingShapeIds())).toEqual(new Set([ids.box1, ids.box2]))
513
+
514
+ // Press accel key to restrict to first shape
515
+ editor.keyDown('Meta')
516
+ // The accel key should affect the erasing behavior
517
+ expect(editor.getErasingShapeIds().length).toBeGreaterThan(0)
518
+
519
+ // Release the accel key
520
+ editor.keyUp('Meta')
521
+
522
+ // Should still include shapes hit
523
+ expect(editor.getErasingShapeIds().length).toBeGreaterThan(0)
524
+
525
+ editor.pointerUp()
526
+
527
+ // Should delete shapes
528
+ const shapesAfterCount = editor.getCurrentPageShapes().length
529
+ expect(shapesAfterCount).toBeLessThan(shapesBeforeCount)
530
+ })
531
+
532
+ it('Prevents pointer move from starting erasing when accel key is held in pointing state (only if there is a first erasing shape)', () => {
533
+ editor.setCurrentTool('eraser')
534
+ editor.expectToBeIn('eraser.idle')
535
+
536
+ // Start with accel key held and click on a shape
537
+ editor.keyDown('Meta')
538
+ editor.pointerDown(0, 0) // in box1
539
+ editor.expectToBeIn('eraser.pointing')
540
+
541
+ expect(editor.getErasingShapeIds()).toEqual([ids.box1])
542
+
543
+ // Try to move pointer - should not start erasing
544
+ editor.pointerMove(50, 50)
545
+ editor.expectToBeIn('eraser.pointing') // Should still be in pointing state
546
+
547
+ editor.pointerUp()
548
+ editor.keyUp('Meta')
549
+ })
550
+
551
+ it('Preserves only first erasing shape when accel key is pressed during erasing (only if there is a first erasing shape)', () => {
552
+ editor.setCurrentTool('eraser')
553
+ editor.expectToBeIn('eraser.idle')
554
+
555
+ const shapesBeforeCount = editor.getCurrentPageShapes().length
556
+
557
+ // Start erasing normally
558
+ editor.pointerDown(-100, -100) // outside of any shapes
559
+ editor.pointerMove(99, 99) // next to box1 AND in box2
560
+
561
+ jest.advanceTimersByTime(16)
562
+ expect(editor.getInstanceState().scribbles.length).toBe(1)
563
+
564
+ // Should include all shapes hit initially
565
+ expect(new Set(editor.getErasingShapeIds())).toEqual(new Set([ids.box1, ids.box2]))
566
+
567
+ // Press accel key during erasing
568
+ editor.keyDown('Meta')
569
+
570
+ // The accel key should affect the erasing behavior
571
+ expect(editor.getErasingShapeIds().length).toBeGreaterThan(0)
572
+
573
+ editor.pointerUp()
574
+
575
+ // Should delete at least one shape
576
+ const shapesAfterCount = editor.getCurrentPageShapes().length
577
+ expect(shapesAfterCount).toBeLessThan(shapesBeforeCount)
578
+
579
+ editor.keyUp('Meta')
580
+ })
581
+
582
+ it('Maintains first shape erasing behavior when accel key is held throughout the erasing session (only if there is a first erasing shape)', () => {
583
+ editor.setCurrentTool('eraser')
584
+ editor.expectToBeIn('eraser.idle')
585
+
586
+ const shapesBeforeCount = editor.getCurrentPageShapes().length
587
+
588
+ // Start dragging without accel key to establish first erasing shape
589
+ editor.pointerDown(-100, -100) // outside of any shapes
590
+ editor.pointerMove(99, 99) // next to box1 AND in box2
591
+
592
+ jest.advanceTimersByTime(16)
593
+ expect(editor.getInstanceState().scribbles.length).toBe(1)
594
+
595
+ // Should include all shapes hit initially
596
+ expect(new Set(editor.getErasingShapeIds())).toEqual(new Set([ids.box1, ids.box2]))
597
+
598
+ // Press accel key to restrict to first shape
599
+ editor.keyDown('Meta')
600
+ expect(editor.getErasingShapeIds().length).toBeGreaterThan(0)
601
+
602
+ // Move to hit more shapes
603
+ editor.pointerMove(350, 350) // in box3
604
+
605
+ // Should still include shapes being erased
606
+ expect(editor.getErasingShapeIds().length).toBeGreaterThan(0)
607
+
608
+ editor.pointerUp()
609
+
610
+ // Should delete at least one shape
611
+ const shapesAfterCount = editor.getCurrentPageShapes().length
612
+ expect(shapesAfterCount).toBeLessThan(shapesBeforeCount)
613
+
614
+ editor.keyUp('Meta')
445
615
  })
446
616
  })
package/tldraw.css CHANGED
@@ -3052,6 +3052,10 @@ it from receiving any pointer events or affecting the cursor. */
3052
3052
  opacity: 1;
3053
3053
  }
3054
3054
 
3055
+ .tlui-main-toolbar__overflow-content {
3056
+ touch-action: none;
3057
+ }
3058
+
3055
3059
  .tlui-main-toolbar__tools [data-toolbar-visible='false'],
3056
3060
  .tlui-main-toolbar__overflow-content [data-toolbar-visible='false'] {
3057
3061
  display: none;