tldraw 4.3.0-canary.da35795ba8e2 → 4.3.0-canary.e52fa5385f86

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 (225) hide show
  1. package/dist-cjs/index.d.ts +17 -5
  2. package/dist-cjs/index.js +2 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/bindings/arrow/ArrowBindingUtil.js.map +2 -2
  5. package/dist-cjs/lib/canvas/TldrawSelectionForeground.js.map +2 -2
  6. package/dist-cjs/lib/defaultExternalContentHandlers.js.map +2 -2
  7. package/dist-cjs/lib/shapes/arrow/ArrowShapeUtil.js.map +2 -2
  8. package/dist-cjs/lib/shapes/arrow/arrowLabel.js.map +2 -2
  9. package/dist-cjs/lib/shapes/arrow/arrowTargetState.js.map +2 -2
  10. package/dist-cjs/lib/shapes/arrow/elbow/elbowArrowSnapLines.js.map +2 -2
  11. package/dist-cjs/lib/shapes/arrow/shared.js.map +2 -2
  12. package/dist-cjs/lib/shapes/arrow/toolStates/Pointing.js.map +2 -2
  13. package/dist-cjs/lib/shapes/bookmark/bookmarks.js.map +2 -2
  14. package/dist-cjs/lib/shapes/draw/toolStates/Drawing.js.map +2 -2
  15. package/dist-cjs/lib/shapes/embed/EmbedShapeUtil.js.map +2 -2
  16. package/dist-cjs/lib/shapes/frame/FrameShapeTool.js.map +1 -1
  17. package/dist-cjs/lib/shapes/geo/toolStates/Pointing.js.map +2 -2
  18. package/dist-cjs/lib/shapes/line/toolStates/Pointing.js.map +2 -2
  19. package/dist-cjs/lib/shapes/note/noteHelpers.js.map +2 -2
  20. package/dist-cjs/lib/shapes/shared/PlainTextLabel.js.map +2 -2
  21. package/dist-cjs/lib/shapes/shared/RichTextLabel.js.map +2 -2
  22. package/dist-cjs/lib/shapes/shared/crop.js +1 -0
  23. package/dist-cjs/lib/shapes/shared/crop.js.map +2 -2
  24. package/dist-cjs/lib/shapes/shared/useEditablePlainText.js.map +2 -2
  25. package/dist-cjs/lib/shapes/shared/useEditableRichText.js.map +2 -2
  26. package/dist-cjs/lib/shapes/text/toolStates/Pointing.js.map +2 -2
  27. package/dist-cjs/lib/tools/EraserTool/childStates/Erasing.js.map +2 -2
  28. package/dist-cjs/lib/tools/EraserTool/childStates/Pointing.js.map +2 -2
  29. package/dist-cjs/lib/tools/SelectTool/DragAndDropManager.js +1 -4
  30. package/dist-cjs/lib/tools/SelectTool/DragAndDropManager.js.map +2 -2
  31. package/dist-cjs/lib/tools/SelectTool/childStates/Brushing.js.map +2 -2
  32. package/dist-cjs/lib/tools/SelectTool/childStates/Crop/children/Idle.js.map +2 -2
  33. package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js +1 -1
  34. package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js.map +2 -2
  35. package/dist-cjs/lib/tools/SelectTool/childStates/EditingShape.js.map +2 -2
  36. package/dist-cjs/lib/tools/SelectTool/childStates/Idle.js.map +2 -2
  37. package/dist-cjs/lib/tools/SelectTool/childStates/PointingArrowLabel.js.map +2 -2
  38. package/dist-cjs/lib/tools/SelectTool/childStates/PointingHandle.js.map +2 -2
  39. package/dist-cjs/lib/tools/SelectTool/childStates/PointingSelection.js.map +2 -2
  40. package/dist-cjs/lib/tools/SelectTool/childStates/Resizing.js.map +2 -2
  41. package/dist-cjs/lib/tools/SelectTool/childStates/ScribbleBrushing.js.map +2 -2
  42. package/dist-cjs/lib/tools/SelectTool/childStates/Translating.js.map +2 -2
  43. package/dist-cjs/lib/ui/components/EditLinkDialog.js +11 -1
  44. package/dist-cjs/lib/ui/components/EditLinkDialog.js.map +2 -2
  45. package/dist-cjs/lib/ui/components/Toolbar/AltTextEditor.js.map +2 -2
  46. package/dist-cjs/lib/ui/components/menu-items.js.map +2 -2
  47. package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js +1 -1
  48. package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js.map +2 -2
  49. package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js +143 -88
  50. package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js.map +2 -2
  51. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js +1 -1
  52. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js.map +2 -2
  53. package/dist-cjs/lib/ui/context/actions.js +1 -2
  54. package/dist-cjs/lib/ui/context/actions.js.map +2 -2
  55. package/dist-cjs/lib/ui/hooks/menu-hooks.js.map +2 -2
  56. package/dist-cjs/lib/ui/hooks/useFlatten.js.map +2 -2
  57. package/dist-cjs/lib/ui/hooks/useTools.js.map +2 -2
  58. package/dist-cjs/lib/ui/version.js +3 -3
  59. package/dist-cjs/lib/ui/version.js.map +1 -1
  60. package/dist-cjs/lib/utils/excalidraw/putExcalidrawContent.js +8 -0
  61. package/dist-cjs/lib/utils/excalidraw/putExcalidrawContent.js.map +2 -2
  62. package/dist-cjs/lib/utils/export/exportAs.js.map +2 -2
  63. package/dist-cjs/lib/utils/frames/frames.js.map +2 -2
  64. package/dist-cjs/lib/utils/text/richText.js +7 -17
  65. package/dist-cjs/lib/utils/text/richText.js.map +3 -3
  66. package/dist-cjs/lib/utils/tldr/buildFromV1Document.js.map +2 -2
  67. package/dist-esm/index.d.mts +17 -5
  68. package/dist-esm/index.mjs +3 -1
  69. package/dist-esm/index.mjs.map +2 -2
  70. package/dist-esm/lib/bindings/arrow/ArrowBindingUtil.mjs.map +2 -2
  71. package/dist-esm/lib/canvas/TldrawSelectionForeground.mjs.map +2 -2
  72. package/dist-esm/lib/defaultExternalContentHandlers.mjs.map +2 -2
  73. package/dist-esm/lib/shapes/arrow/ArrowShapeUtil.mjs.map +2 -2
  74. package/dist-esm/lib/shapes/arrow/arrowLabel.mjs.map +2 -2
  75. package/dist-esm/lib/shapes/arrow/arrowTargetState.mjs.map +2 -2
  76. package/dist-esm/lib/shapes/arrow/elbow/elbowArrowSnapLines.mjs.map +2 -2
  77. package/dist-esm/lib/shapes/arrow/shared.mjs.map +2 -2
  78. package/dist-esm/lib/shapes/arrow/toolStates/Pointing.mjs.map +2 -2
  79. package/dist-esm/lib/shapes/bookmark/bookmarks.mjs.map +2 -2
  80. package/dist-esm/lib/shapes/draw/toolStates/Drawing.mjs.map +2 -2
  81. package/dist-esm/lib/shapes/embed/EmbedShapeUtil.mjs.map +2 -2
  82. package/dist-esm/lib/shapes/frame/FrameShapeTool.mjs.map +1 -1
  83. package/dist-esm/lib/shapes/geo/toolStates/Pointing.mjs.map +2 -2
  84. package/dist-esm/lib/shapes/line/toolStates/Pointing.mjs.map +2 -2
  85. package/dist-esm/lib/shapes/note/noteHelpers.mjs.map +2 -2
  86. package/dist-esm/lib/shapes/shared/PlainTextLabel.mjs.map +2 -2
  87. package/dist-esm/lib/shapes/shared/RichTextLabel.mjs.map +2 -2
  88. package/dist-esm/lib/shapes/shared/crop.mjs +1 -0
  89. package/dist-esm/lib/shapes/shared/crop.mjs.map +2 -2
  90. package/dist-esm/lib/shapes/shared/useEditablePlainText.mjs.map +2 -2
  91. package/dist-esm/lib/shapes/shared/useEditableRichText.mjs.map +2 -2
  92. package/dist-esm/lib/shapes/text/toolStates/Pointing.mjs.map +2 -2
  93. package/dist-esm/lib/tools/EraserTool/childStates/Erasing.mjs.map +2 -2
  94. package/dist-esm/lib/tools/EraserTool/childStates/Pointing.mjs +1 -4
  95. package/dist-esm/lib/tools/EraserTool/childStates/Pointing.mjs.map +2 -2
  96. package/dist-esm/lib/tools/SelectTool/DragAndDropManager.mjs +1 -4
  97. package/dist-esm/lib/tools/SelectTool/DragAndDropManager.mjs.map +2 -2
  98. package/dist-esm/lib/tools/SelectTool/childStates/Brushing.mjs.map +2 -2
  99. package/dist-esm/lib/tools/SelectTool/childStates/Crop/children/Idle.mjs.map +2 -2
  100. package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs +1 -1
  101. package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs.map +2 -2
  102. package/dist-esm/lib/tools/SelectTool/childStates/EditingShape.mjs.map +2 -2
  103. package/dist-esm/lib/tools/SelectTool/childStates/Idle.mjs.map +2 -2
  104. package/dist-esm/lib/tools/SelectTool/childStates/PointingArrowLabel.mjs.map +2 -2
  105. package/dist-esm/lib/tools/SelectTool/childStates/PointingHandle.mjs +1 -4
  106. package/dist-esm/lib/tools/SelectTool/childStates/PointingHandle.mjs.map +2 -2
  107. package/dist-esm/lib/tools/SelectTool/childStates/PointingSelection.mjs.map +2 -2
  108. package/dist-esm/lib/tools/SelectTool/childStates/Resizing.mjs.map +2 -2
  109. package/dist-esm/lib/tools/SelectTool/childStates/ScribbleBrushing.mjs.map +2 -2
  110. package/dist-esm/lib/tools/SelectTool/childStates/Translating.mjs.map +2 -2
  111. package/dist-esm/lib/ui/components/EditLinkDialog.mjs +11 -1
  112. package/dist-esm/lib/ui/components/EditLinkDialog.mjs.map +2 -2
  113. package/dist-esm/lib/ui/components/Toolbar/AltTextEditor.mjs.map +2 -2
  114. package/dist-esm/lib/ui/components/menu-items.mjs +1 -4
  115. package/dist-esm/lib/ui/components/menu-items.mjs.map +2 -2
  116. package/dist-esm/lib/ui/components/primitives/TldrawUiSlider.mjs +2 -2
  117. package/dist-esm/lib/ui/components/primitives/TldrawUiSlider.mjs.map +2 -2
  118. package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs +151 -90
  119. package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs.map +2 -2
  120. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs +2 -2
  121. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs.map +2 -2
  122. package/dist-esm/lib/ui/context/actions.mjs +1 -2
  123. package/dist-esm/lib/ui/context/actions.mjs.map +2 -2
  124. package/dist-esm/lib/ui/hooks/menu-hooks.mjs +1 -4
  125. package/dist-esm/lib/ui/hooks/menu-hooks.mjs.map +2 -2
  126. package/dist-esm/lib/ui/hooks/useFlatten.mjs.map +2 -2
  127. package/dist-esm/lib/ui/hooks/useTools.mjs.map +2 -2
  128. package/dist-esm/lib/ui/version.mjs +3 -3
  129. package/dist-esm/lib/ui/version.mjs.map +1 -1
  130. package/dist-esm/lib/utils/excalidraw/putExcalidrawContent.mjs +8 -0
  131. package/dist-esm/lib/utils/excalidraw/putExcalidrawContent.mjs.map +2 -2
  132. package/dist-esm/lib/utils/export/exportAs.mjs +1 -3
  133. package/dist-esm/lib/utils/export/exportAs.mjs.map +2 -2
  134. package/dist-esm/lib/utils/frames/frames.mjs.map +2 -2
  135. package/dist-esm/lib/utils/text/richText.mjs +3 -3
  136. package/dist-esm/lib/utils/text/richText.mjs.map +2 -2
  137. package/dist-esm/lib/utils/tldr/buildFromV1Document.mjs.map +2 -2
  138. package/package.json +10 -10
  139. package/src/index.ts +1 -0
  140. package/src/lib/bindings/arrow/ArrowBindingUtil.ts +1 -1
  141. package/src/lib/canvas/TldrawSelectionForeground.tsx +4 -9
  142. package/src/lib/defaultExternalContentHandlers.ts +3 -4
  143. package/src/lib/shapes/arrow/ArrowShapeUtil.test.ts +2 -2
  144. package/src/lib/shapes/arrow/ArrowShapeUtil.tsx +1 -1
  145. package/src/lib/shapes/arrow/arrowLabel.ts +1 -1
  146. package/src/lib/shapes/arrow/arrowTargetState.ts +1 -1
  147. package/src/lib/shapes/arrow/elbow/elbowArrowSnapLines.tsx +3 -3
  148. package/src/lib/shapes/arrow/shared.ts +4 -4
  149. package/src/lib/shapes/arrow/toolStates/Pointing.tsx +1 -1
  150. package/src/lib/shapes/bookmark/bookmarks.ts +3 -3
  151. package/src/lib/shapes/draw/toolStates/Drawing.ts +4 -4
  152. package/src/lib/shapes/embed/EmbedShapeUtil.tsx +1 -1
  153. package/src/lib/shapes/frame/FrameShapeTool.ts +1 -1
  154. package/src/lib/shapes/geo/GeoShapeUtil.test.tsx +10 -2
  155. package/src/lib/shapes/geo/toolStates/Pointing.ts +3 -3
  156. package/src/lib/shapes/line/LineShapeTool.test.ts +6 -6
  157. package/src/lib/shapes/line/LineShapeUtil.test.tsx +5 -5
  158. package/src/lib/shapes/line/toolStates/Pointing.ts +1 -1
  159. package/src/lib/shapes/note/NoteShapeTool.test.ts +2 -1
  160. package/src/lib/shapes/note/noteHelpers.ts +2 -2
  161. package/src/lib/shapes/shared/PlainTextLabel.tsx +2 -1
  162. package/src/lib/shapes/shared/RichTextLabel.tsx +2 -1
  163. package/src/lib/shapes/shared/crop.ts +1 -0
  164. package/src/lib/shapes/shared/useEditablePlainText.ts +7 -3
  165. package/src/lib/shapes/shared/useEditableRichText.ts +7 -3
  166. package/src/lib/shapes/text/TextShapeTool.test.ts +4 -4
  167. package/src/lib/shapes/text/toolStates/Pointing.ts +1 -1
  168. package/src/lib/tools/EraserTool/childStates/Erasing.ts +3 -5
  169. package/src/lib/tools/EraserTool/childStates/Pointing.ts +3 -16
  170. package/src/lib/tools/SelectTool/DragAndDropManager.ts +2 -4
  171. package/src/lib/tools/SelectTool/childStates/Brushing.ts +2 -6
  172. package/src/lib/tools/SelectTool/childStates/Crop/children/Idle.ts +2 -3
  173. package/src/lib/tools/SelectTool/childStates/DraggingHandle.tsx +4 -7
  174. package/src/lib/tools/SelectTool/childStates/EditingShape.ts +2 -4
  175. package/src/lib/tools/SelectTool/childStates/Idle.ts +6 -10
  176. package/src/lib/tools/SelectTool/childStates/PointingArrowLabel.ts +1 -1
  177. package/src/lib/tools/SelectTool/childStates/PointingHandle.ts +4 -12
  178. package/src/lib/tools/SelectTool/childStates/PointingSelection.ts +2 -2
  179. package/src/lib/tools/SelectTool/childStates/Resizing.ts +2 -4
  180. package/src/lib/tools/SelectTool/childStates/ScribbleBrushing.ts +2 -4
  181. package/src/lib/tools/SelectTool/childStates/Translating.ts +1 -3
  182. package/src/lib/ui/components/EditLinkDialog.tsx +16 -6
  183. package/src/lib/ui/components/Toolbar/AltTextEditor.tsx +2 -2
  184. package/src/lib/ui/components/menu-items.tsx +6 -14
  185. package/src/lib/ui/components/primitives/TldrawUiSlider.tsx +2 -2
  186. package/src/lib/ui/components/primitives/TldrawUiTooltip.tsx +196 -108
  187. package/src/lib/ui/components/primitives/menus/TldrawUiMenuItem.tsx +2 -2
  188. package/src/lib/ui/context/actions.tsx +9 -13
  189. package/src/lib/ui/hooks/menu-hooks.ts +9 -19
  190. package/src/lib/ui/hooks/useFlatten.ts +1 -2
  191. package/src/lib/ui/hooks/useTools.tsx +1 -2
  192. package/src/lib/ui/version.ts +3 -3
  193. package/src/lib/utils/excalidraw/putExcalidrawContent.ts +8 -0
  194. package/src/lib/utils/export/exportAs.ts +2 -9
  195. package/src/lib/utils/frames/frames.ts +1 -1
  196. package/src/lib/utils/text/richText.ts +3 -3
  197. package/src/lib/utils/tldr/buildFromV1Document.ts +12 -17
  198. package/src/test/Editor.test.tsx +38 -12
  199. package/src/test/SelectTool.test.ts +11 -19
  200. package/src/test/TestEditor.ts +1 -4
  201. package/src/test/TldrawEditor.test.tsx +21 -18
  202. package/src/test/bindings.test.tsx +29 -25
  203. package/src/test/bindingsIndex.test.tsx +4 -4
  204. package/src/test/commands/createShape.test.ts +64 -0
  205. package/src/test/commands/createShapes.test.ts +15 -1
  206. package/src/test/commands/getSvgString.test.ts +2 -2
  207. package/src/test/commands/isShapeOfType.test.ts +44 -0
  208. package/src/test/commands/putContent.test.ts +80 -1
  209. package/src/test/commands/updateShape.test.ts +67 -0
  210. package/src/test/commands/updateShapes.test.ts +21 -5
  211. package/src/test/custom-clipping.test.ts +36 -35
  212. package/src/test/customSnapping.test.tsx +77 -62
  213. package/src/test/duplicate.test.ts +1 -1
  214. package/src/test/frames.test.ts +2 -2
  215. package/src/test/getCulledShapes.test.tsx +11 -3
  216. package/src/test/getShapeAtPoint.test.ts +2 -2
  217. package/src/test/groups.test.tsx +6 -3
  218. package/src/test/resizing.test.ts +9 -13
  219. package/src/test/selection-omnibus.test.ts +11 -11
  220. package/src/test/shapeutils.test.ts +1 -1
  221. package/src/test/styles2.test.tsx +1 -1
  222. package/src/test/styles3.test.ts +5 -5
  223. package/src/test/test-jsx.tsx +69 -57
  224. package/src/test/text.test.ts +15 -17
  225. package/src/test/translating.test.ts +6 -8
@@ -1,8 +1,6 @@
1
1
  import {
2
2
  Geometry2d,
3
3
  StateNode,
4
- TLFrameShape,
5
- TLGroupShape,
6
4
  TLShape,
7
5
  TLShapeId,
8
6
  Vec,
@@ -104,7 +102,7 @@ export class ScribbleBrushing extends StateNode {
104
102
 
105
103
  // If the shape is a group or is already selected or locked, don't select it
106
104
  if (
107
- editor.isShapeOfType<TLGroupShape>(shape, 'group') ||
105
+ editor.isShapeOfType(shape, 'group') ||
108
106
  newlySelectedShapeIds.has(shape.id) ||
109
107
  editor.isShapeOrAncestorLocked(shape)
110
108
  ) {
@@ -115,7 +113,7 @@ export class ScribbleBrushing extends StateNode {
115
113
 
116
114
  // If the scribble started inside of the frame, don't select it
117
115
  if (
118
- editor.isShapeOfType<TLFrameShape>(shape, 'frame') &&
116
+ editor.isShapeOfType(shape, 'frame') &&
119
117
  geometry.bounds.containsPoint(editor.getPointInShapeSpace(shape, originPagePoint))
120
118
  ) {
121
119
  continue
@@ -407,9 +407,7 @@ function getTranslatingSnapshot(editor: Editor) {
407
407
  const { originPagePoint } = editor.inputs
408
408
 
409
409
  const allHoveredNotes = shapeSnapshots.filter(
410
- (s) =>
411
- editor.isShapeOfType<TLNoteShape>(s.shape, 'note') &&
412
- editor.isPointInShape(s.shape, originPagePoint)
410
+ (s) => editor.isShapeOfType(s.shape, 'note') && editor.isPointInShape(s.shape, originPagePoint)
413
411
  ) as (MovingShapeSnapshot & { shape: TLNoteShape })[]
414
412
 
415
413
  if (allHoveredNotes.length === 0) {
@@ -1,4 +1,4 @@
1
- import { T, TLBaseShape, track, useEditor } from '@tldraw/editor'
1
+ import { ExtractShapeByProps, T, TLShape, track, useEditor } from '@tldraw/editor'
2
2
  import { useCallback, useEffect, useRef, useState } from 'react'
3
3
  import { TLUiDialogProps } from '../context/dialogs'
4
4
  import { useTranslation } from '../hooks/useTranslation/useTranslation'
@@ -25,20 +25,28 @@ function validateUrl(url: string) {
25
25
  return { isValid: false, hasProtocol: false }
26
26
  }
27
27
 
28
- type ShapeWithUrl = TLBaseShape<string, { url: string }>
28
+ type ShapeWithUrl = ExtractShapeByProps<{ url: string }>
29
+
30
+ function isShapeWithUrl(shape: TLShape | null | undefined): shape is ShapeWithUrl {
31
+ return !!(shape && 'url' in shape.props && typeof shape.props.url === 'string')
32
+ }
33
+
34
+ function assertShapeWithUrl(shape: TLShape | null | undefined): asserts shape is ShapeWithUrl {
35
+ if (!isShapeWithUrl(shape)) {
36
+ throw new Error('Shape is not a valid ShapeWithUrl')
37
+ }
38
+ }
29
39
 
30
40
  export const EditLinkDialog = track(function EditLinkDialog({ onClose }: TLUiDialogProps) {
31
41
  const editor = useEditor()
32
42
 
33
43
  const selectedShape = editor.getOnlySelectedShape()
34
44
 
35
- if (
36
- !(selectedShape && 'url' in selectedShape.props && typeof selectedShape.props.url === 'string')
37
- ) {
45
+ if (!isShapeWithUrl(selectedShape)) {
38
46
  return null
39
47
  }
40
48
 
41
- return <EditLinkDialogInner onClose={onClose} selectedShape={selectedShape as ShapeWithUrl} />
49
+ return <EditLinkDialogInner onClose={onClose} selectedShape={selectedShape} />
42
50
  })
43
51
 
44
52
  export const EditLinkDialogInner = track(function EditLinkDialogInner({
@@ -98,6 +106,7 @@ export const EditLinkDialogInner = track(function EditLinkDialogInner({
98
106
  const handleClear = useCallback(() => {
99
107
  const onlySelectedShape = editor.getOnlySelectedShape()
100
108
  if (!onlySelectedShape) return
109
+ assertShapeWithUrl(onlySelectedShape)
101
110
  editor.updateShapes([
102
111
  { id: onlySelectedShape.id, type: onlySelectedShape.type, props: { url: '' } },
103
112
  ])
@@ -108,6 +117,7 @@ export const EditLinkDialogInner = track(function EditLinkDialogInner({
108
117
  const onlySelectedShape = editor.getOnlySelectedShape()
109
118
 
110
119
  if (!onlySelectedShape) return
120
+ assertShapeWithUrl(onlySelectedShape)
111
121
 
112
122
  // ? URL is a magic value
113
123
  if (onlySelectedShape && 'url' in onlySelectedShape.props) {
@@ -1,4 +1,4 @@
1
- import { preventDefault, TLShape, TLShapeId, useEditor } from '@tldraw/editor'
1
+ import { ExtractShapeByProps, preventDefault, TLShape, TLShapeId, useEditor } from '@tldraw/editor'
2
2
  import { useCallback, useEffect, useRef, useState } from 'react'
3
3
  import { useUiEvents } from '../../context/events'
4
4
  import { useTranslation } from '../../hooks/useTranslation/useTranslation'
@@ -31,7 +31,7 @@ export function AltTextEditor({ shapeId, onClose, source }: AltTextEditorProps)
31
31
 
32
32
  const handleComplete = () => {
33
33
  trackEvent('set-alt-text', { source })
34
- const shape = editor.getShape<TLShape & { props: { altText: string } }>(shapeId)
34
+ const shape = editor.getShape<ExtractShapeByProps<{ altText: string }>>(shapeId)
35
35
  if (!shape) return
36
36
  editor.updateShapes([
37
37
  {
@@ -1,12 +1,4 @@
1
- import {
2
- TLBookmarkShape,
3
- TLEmbedShape,
4
- TLFrameShape,
5
- TLImageShape,
6
- TLPageId,
7
- useEditor,
8
- useValue,
9
- } from '@tldraw/editor'
1
+ import { TLPageId, useEditor, useValue } from '@tldraw/editor'
10
2
  import { supportsDownloadingOriginal } from '../context/actions'
11
3
  import { useUiEvents } from '../context/events'
12
4
  import { useToasts } from '../context/toasts'
@@ -64,7 +56,7 @@ export function FlattenMenuItem() {
64
56
  const selectedShapeIds = editor.getSelectedShapeIds()
65
57
  if (selectedShapeIds.length === 0) return false
66
58
  const onlySelectedShape = editor.getOnlySelectedShape()
67
- if (onlySelectedShape && editor.isShapeOfType<TLImageShape>(onlySelectedShape, 'image')) {
59
+ if (onlySelectedShape && editor.isShapeOfType(onlySelectedShape, 'image')) {
68
60
  return false
69
61
  }
70
62
  return true
@@ -117,7 +109,7 @@ export function RemoveFrameMenuItem() {
117
109
  () => {
118
110
  const selectedShapes = editor.getSelectedShapes()
119
111
  if (selectedShapes.length === 0) return false
120
- return selectedShapes.every((shape) => editor.isShapeOfType<TLFrameShape>(shape, 'frame'))
112
+ return selectedShapes.every((shape) => editor.isShapeOfType(shape, 'frame'))
121
113
  },
122
114
  [editor]
123
115
  )
@@ -135,7 +127,7 @@ export function FitFrameToContentMenuItem() {
135
127
  const onlySelectedShape = editor.getOnlySelectedShape()
136
128
  if (!onlySelectedShape) return false
137
129
  return (
138
- editor.isShapeOfType<TLFrameShape>(onlySelectedShape, 'frame') &&
130
+ editor.isShapeOfType(onlySelectedShape, 'frame') &&
139
131
  editor.getSortedChildIdsForParent(onlySelectedShape).length > 0
140
132
  )
141
133
  },
@@ -518,7 +510,7 @@ export function ConvertToBookmarkMenuItem() {
518
510
  const onlySelectedShape = editor.getOnlySelectedShape()
519
511
  if (!onlySelectedShape) return false
520
512
  return !!(
521
- editor.isShapeOfType<TLEmbedShape>(onlySelectedShape, 'embed') &&
513
+ editor.isShapeOfType(onlySelectedShape, 'embed') &&
522
514
  onlySelectedShape.props.url &&
523
515
  !editor.isShapeOrAncestorLocked(onlySelectedShape)
524
516
  )
@@ -542,7 +534,7 @@ export function ConvertToEmbedMenuItem() {
542
534
  const onlySelectedShape = editor.getOnlySelectedShape()
543
535
  if (!onlySelectedShape) return false
544
536
  return !!(
545
- editor.isShapeOfType<TLBookmarkShape>(onlySelectedShape, 'bookmark') &&
537
+ editor.isShapeOfType(onlySelectedShape, 'bookmark') &&
546
538
  onlySelectedShape.props.url &&
547
539
  getEmbedDefinition(onlySelectedShape.props.url) &&
548
540
  !editor.isShapeOrAncestorLocked(onlySelectedShape)
@@ -3,7 +3,7 @@ import { Slider as _Slider } from 'radix-ui'
3
3
  import React, { useCallback, useEffect, useState } from 'react'
4
4
  import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey'
5
5
  import { useTranslation } from '../../hooks/useTranslation/useTranslation'
6
- import { TldrawUiTooltip, tooltipManager } from './TldrawUiTooltip'
6
+ import { hideAllTooltips, TldrawUiTooltip } from './TldrawUiTooltip'
7
7
 
8
8
  /** @public */
9
9
  export interface TLUiSliderProps {
@@ -52,7 +52,7 @@ export const TldrawUiSlider = React.forwardRef<HTMLDivElement, TLUiSliderProps>(
52
52
  )
53
53
 
54
54
  const handlePointerDown = useCallback(() => {
55
- tooltipManager.hideAllTooltips()
55
+ hideAllTooltips()
56
56
  onHistoryMark?.('click slider')
57
57
  }, [onHistoryMark])
58
58
 
@@ -1,4 +1,12 @@
1
- import { assert, Atom, atom, Editor, uniqueId, useMaybeEditor, useValue } from '@tldraw/editor'
1
+ import {
2
+ assert,
3
+ atom,
4
+ Editor,
5
+ tlenvReactive,
6
+ uniqueId,
7
+ useMaybeEditor,
8
+ useValue,
9
+ } from '@tldraw/editor'
2
10
  import { Tooltip as _Tooltip } from 'radix-ui'
3
11
  import React, {
4
12
  createContext,
@@ -6,7 +14,6 @@ import React, {
6
14
  ReactNode,
7
15
  useContext,
8
16
  useEffect,
9
- useLayoutEffect,
10
17
  useRef,
11
18
  useState,
12
19
  } from 'react'
@@ -25,7 +32,7 @@ export interface TldrawUiTooltipProps {
25
32
  delayDuration?: number
26
33
  }
27
34
 
28
- interface CurrentTooltip {
35
+ interface TooltipData {
29
36
  id: string
30
37
  content: ReactNode
31
38
  side: 'top' | 'right' | 'bottom' | 'left'
@@ -35,11 +42,25 @@ interface CurrentTooltip {
35
42
  delayDuration: number
36
43
  }
37
44
 
38
- // Singleton tooltip manager
45
+ // State machine states
46
+ type TooltipState =
47
+ | { name: 'idle' }
48
+ | { name: 'pointer_down' }
49
+ | { name: 'showing'; tooltip: TooltipData }
50
+ | { name: 'waiting_to_hide'; tooltip: TooltipData; timeoutId: number }
51
+
52
+ // State machine events
53
+ type TooltipEvent =
54
+ | { type: 'pointer_down' }
55
+ | { type: 'pointer_up' }
56
+ | { type: 'show'; tooltip: TooltipData }
57
+ | { type: 'hide'; tooltipId: string; editor: Editor | null; instant: boolean }
58
+ | { type: 'hide_all' }
59
+
60
+ // Singleton tooltip manager using explicit state machine
39
61
  class TooltipManager {
40
62
  private static instance: TooltipManager | null = null
41
- private currentTooltip = atom<CurrentTooltip | null>('current tooltip', null)
42
- private destroyTimeoutId: number | null = null
63
+ private state = atom<TooltipState>('tooltip state', { name: 'idle' })
43
64
 
44
65
  static getInstance(): TooltipManager {
45
66
  if (!TooltipManager.instance) {
@@ -48,86 +69,117 @@ class TooltipManager {
48
69
  return TooltipManager.instance
49
70
  }
50
71
 
51
- showTooltip(
52
- tooltipId: string,
53
- content: string | React.ReactNode,
54
- targetElement: HTMLElement,
55
- side: 'top' | 'right' | 'bottom' | 'left',
56
- sideOffset: number,
57
- showOnMobile: boolean,
58
- delayDuration: number
59
- ) {
60
- // Clear any existing destroy timeout
61
- if (this.destroyTimeoutId) {
62
- clearTimeout(this.destroyTimeoutId)
63
- this.destroyTimeoutId = null
64
- }
65
-
66
- // Update current tooltip
67
- this.currentTooltip.set({
68
- id: tooltipId,
69
- content,
70
- side,
71
- sideOffset,
72
- showOnMobile,
73
- targetElement,
74
- delayDuration,
75
- })
72
+ hideAllTooltips() {
73
+ this.handleEvent({ type: 'hide_all' })
76
74
  }
77
75
 
78
- updateCurrentTooltip(tooltipId: string, update: (tooltip: CurrentTooltip) => CurrentTooltip) {
79
- this.currentTooltip.update((tooltip) => {
80
- if (tooltip?.id === tooltipId) {
81
- return update(tooltip)
76
+ handleEvent(event: TooltipEvent) {
77
+ const currentState = this.state.get()
78
+
79
+ switch (event.type) {
80
+ case 'pointer_down': {
81
+ // Transition to pointer_down from any state
82
+ if (currentState.name === 'waiting_to_hide') {
83
+ clearTimeout(currentState.timeoutId)
84
+ }
85
+ this.state.set({ name: 'pointer_down' })
86
+ break
82
87
  }
83
- return tooltip
84
- })
85
- }
86
88
 
87
- hideTooltip(editor: Editor | null, tooltipId: string, instant: boolean = false) {
88
- const hide = () => {
89
- // Only hide if this is the current tooltip
90
- if (this.currentTooltip.get()?.id === tooltipId) {
91
- this.currentTooltip.set(null)
92
- this.destroyTimeoutId = null
89
+ case 'pointer_up': {
90
+ // Only transition from pointer_down to idle
91
+ if (currentState.name === 'pointer_down') {
92
+ this.state.set({ name: 'idle' })
93
+ }
94
+ break
93
95
  }
94
- }
95
96
 
96
- if (editor && !instant) {
97
- // Start destroy timeout (1 second)
98
- this.destroyTimeoutId = editor.timers.setTimeout(hide, 300)
99
- } else {
100
- hide()
101
- }
102
- }
97
+ case 'show': {
98
+ // Don't show tooltips while pointer is down
99
+ if (currentState.name === 'pointer_down') {
100
+ return
101
+ }
103
102
 
104
- hideAllTooltips() {
105
- this.currentTooltip.set(null)
106
- this.destroyTimeoutId = null
107
- }
103
+ // Clear any existing timeout if transitioning from waiting_to_hide
104
+ if (currentState.name === 'waiting_to_hide') {
105
+ clearTimeout(currentState.timeoutId)
106
+ }
107
+
108
+ // Transition to showing state
109
+ this.state.set({ name: 'showing', tooltip: event.tooltip })
110
+ break
111
+ }
112
+
113
+ case 'hide': {
114
+ const { tooltipId, editor, instant } = event
115
+
116
+ // Only hide if the tooltip matches
117
+ if (currentState.name === 'showing' && currentState.tooltip.id === tooltipId) {
118
+ if (editor && !instant) {
119
+ // Transition to waiting_to_hide state
120
+ const timeoutId = editor.timers.setTimeout(() => {
121
+ const state = this.state.get()
122
+ if (state.name === 'waiting_to_hide' && state.tooltip.id === tooltipId) {
123
+ this.state.set({ name: 'idle' })
124
+ }
125
+ }, 300)
126
+ this.state.set({
127
+ name: 'waiting_to_hide',
128
+ tooltip: currentState.tooltip,
129
+ timeoutId,
130
+ })
131
+ } else {
132
+ this.state.set({ name: 'idle' })
133
+ }
134
+ } else if (
135
+ currentState.name === 'waiting_to_hide' &&
136
+ currentState.tooltip.id === tooltipId
137
+ ) {
138
+ // Already waiting to hide, make it instant if requested
139
+ if (instant) {
140
+ clearTimeout(currentState.timeoutId)
141
+ this.state.set({ name: 'idle' })
142
+ }
143
+ }
144
+ break
145
+ }
108
146
 
109
- getCurrentTooltipData() {
110
- const currentTooltip = this.currentTooltip.get()
111
- if (!currentTooltip) return null
112
- if (!this.supportsHover() && !currentTooltip.showOnMobile) return null
113
- return currentTooltip
147
+ case 'hide_all': {
148
+ if (currentState.name === 'waiting_to_hide') {
149
+ clearTimeout(currentState.timeoutId)
150
+ }
151
+ // Preserve pointer_down state if that's the current state
152
+ if (currentState.name === 'pointer_down') {
153
+ return
154
+ }
155
+ this.state.set({ name: 'idle' })
156
+ break
157
+ }
158
+ }
114
159
  }
115
160
 
116
- private supportsHoverAtom: Atom<boolean> | null = null
117
- supportsHover() {
118
- if (!this.supportsHoverAtom) {
119
- const mediaQuery = window.matchMedia('(hover: hover)')
120
- const supportsHover = atom('has hover', mediaQuery.matches)
121
- this.supportsHoverAtom = supportsHover
122
- mediaQuery.addEventListener('change', (e) => {
123
- supportsHover.set(e.matches)
124
- })
161
+ getCurrentTooltipData(): TooltipData | null {
162
+ const currentState = this.state.get()
163
+ let tooltip: TooltipData | null = null
164
+
165
+ if (currentState.name === 'showing') {
166
+ tooltip = currentState.tooltip
167
+ } else if (currentState.name === 'waiting_to_hide') {
168
+ tooltip = currentState.tooltip
125
169
  }
126
- return this.supportsHoverAtom.get()
170
+
171
+ if (!tooltip) return null
172
+ if (tlenvReactive.get().isCoarsePointer && !tooltip.showOnMobile) return null
173
+ return tooltip
127
174
  }
128
175
  }
129
176
 
130
- export const tooltipManager = TooltipManager.getInstance()
177
+ const tooltipManager = TooltipManager.getInstance()
178
+
179
+ /** @public */
180
+ export function hideAllTooltips() {
181
+ tooltipManager.hideAllTooltips()
182
+ }
131
183
 
132
184
  // Context for the tooltip singleton
133
185
  const TooltipSingletonContext = createContext<boolean>(false)
@@ -167,14 +219,19 @@ function TooltipSingleton() {
167
219
  // Hide tooltip when camera is moving (panning/zooming)
168
220
  useEffect(() => {
169
221
  if (cameraState === 'moving' && isOpen && currentTooltip) {
170
- tooltipManager.hideTooltip(editor, currentTooltip.id, true)
222
+ tooltipManager.handleEvent({
223
+ type: 'hide',
224
+ tooltipId: currentTooltip.id,
225
+ editor,
226
+ instant: true,
227
+ })
171
228
  }
172
229
  }, [cameraState, isOpen, currentTooltip, editor])
173
230
 
174
231
  useEffect(() => {
175
232
  function handleKeyDown(event: KeyboardEvent) {
176
233
  if (event.key === 'Escape' && currentTooltip && isOpen) {
177
- tooltipManager.hideTooltip(editor, currentTooltip.id)
234
+ hideAllTooltips()
178
235
  event.stopPropagation()
179
236
  }
180
237
  }
@@ -183,7 +240,29 @@ function TooltipSingleton() {
183
240
  return () => {
184
241
  document.removeEventListener('keydown', handleKeyDown, { capture: true })
185
242
  }
186
- }, [editor, currentTooltip, isOpen])
243
+ }, [currentTooltip, isOpen])
244
+
245
+ // Hide tooltip and prevent new ones from opening while pointer is down
246
+ useEffect(() => {
247
+ function handlePointerDown() {
248
+ tooltipManager.handleEvent({ type: 'pointer_down' })
249
+ }
250
+
251
+ function handlePointerUp() {
252
+ tooltipManager.handleEvent({ type: 'pointer_up' })
253
+ }
254
+
255
+ document.addEventListener('pointerdown', handlePointerDown, { capture: true })
256
+ document.addEventListener('pointerup', handlePointerUp, { capture: true })
257
+ document.addEventListener('pointercancel', handlePointerUp, { capture: true })
258
+ return () => {
259
+ document.removeEventListener('pointerdown', handlePointerDown, { capture: true })
260
+ document.removeEventListener('pointerup', handlePointerUp, { capture: true })
261
+ document.removeEventListener('pointercancel', handlePointerUp, { capture: true })
262
+ // Reset pointer state on unmount to prevent stuck state
263
+ tooltipManager.handleEvent({ type: 'pointer_up' })
264
+ }
265
+ }, [])
187
266
 
188
267
  // Update open state and trigger position
189
268
  useEffect(() => {
@@ -280,23 +359,16 @@ export const TldrawUiTooltip = forwardRef<HTMLButtonElement, TldrawUiTooltipProp
280
359
  const currentTooltipId = tooltipId.current
281
360
  return () => {
282
361
  if (hasProvider) {
283
- tooltipManager.hideTooltip(editor, currentTooltipId, true)
362
+ tooltipManager.handleEvent({
363
+ type: 'hide',
364
+ tooltipId: currentTooltipId,
365
+ editor,
366
+ instant: true,
367
+ })
284
368
  }
285
369
  }
286
370
  }, [editor, hasProvider])
287
371
 
288
- useLayoutEffect(() => {
289
- if (hasProvider && tooltipManager.getCurrentTooltipData()?.id === tooltipId.current) {
290
- tooltipManager.updateCurrentTooltip(tooltipId.current, (tooltip) => ({
291
- ...tooltip,
292
- content,
293
- side: sideToUse,
294
- sideOffset,
295
- showOnMobile,
296
- }))
297
- }
298
- }, [content, sideToUse, sideOffset, showOnMobile, hasProvider])
299
-
300
372
  // Don't show tooltip if disabled, no content, or enhanced accessibility mode is disabled
301
373
  if (disabled || !content) {
302
374
  return <>{children}</>
@@ -340,38 +412,54 @@ export const TldrawUiTooltip = forwardRef<HTMLButtonElement, TldrawUiTooltipProp
340
412
 
341
413
  const handleMouseEnter = (event: React.MouseEvent<HTMLElement>) => {
342
414
  child.props.onMouseEnter?.(event)
343
- tooltipManager.showTooltip(
344
- tooltipId.current,
345
- content,
346
- event.currentTarget as HTMLElement,
347
- sideToUse,
348
- sideOffset,
349
- showOnMobile,
350
- delayDurationToUse
351
- )
415
+ tooltipManager.handleEvent({
416
+ type: 'show',
417
+ tooltip: {
418
+ id: tooltipId.current,
419
+ content,
420
+ targetElement: event.currentTarget as HTMLElement,
421
+ side: sideToUse,
422
+ sideOffset,
423
+ showOnMobile,
424
+ delayDuration: delayDurationToUse,
425
+ },
426
+ })
352
427
  }
353
428
 
354
429
  const handleMouseLeave = (event: React.MouseEvent<HTMLElement>) => {
355
430
  child.props.onMouseLeave?.(event)
356
- tooltipManager.hideTooltip(editor, tooltipId.current)
431
+ tooltipManager.handleEvent({
432
+ type: 'hide',
433
+ tooltipId: tooltipId.current,
434
+ editor,
435
+ instant: false,
436
+ })
357
437
  }
358
438
 
359
439
  const handleFocus = (event: React.FocusEvent<HTMLElement>) => {
360
440
  child.props.onFocus?.(event)
361
- tooltipManager.showTooltip(
362
- tooltipId.current,
363
- content,
364
- event.currentTarget as HTMLElement,
365
- sideToUse,
366
- sideOffset,
367
- showOnMobile,
368
- delayDurationToUse
369
- )
441
+ tooltipManager.handleEvent({
442
+ type: 'show',
443
+ tooltip: {
444
+ id: tooltipId.current,
445
+ content,
446
+ targetElement: event.currentTarget as HTMLElement,
447
+ side: sideToUse,
448
+ sideOffset,
449
+ showOnMobile,
450
+ delayDuration: delayDurationToUse,
451
+ },
452
+ })
370
453
  }
371
454
 
372
455
  const handleBlur = (event: React.FocusEvent<HTMLElement>) => {
373
456
  child.props.onBlur?.(event)
374
- tooltipManager.hideTooltip(editor, tooltipId.current)
457
+ tooltipManager.handleEvent({
458
+ type: 'hide',
459
+ tooltipId: tooltipId.current,
460
+ editor,
461
+ instant: false,
462
+ })
375
463
  }
376
464
 
377
465
  const childrenWithHandlers = React.cloneElement(children as React.ReactElement, {
@@ -24,7 +24,7 @@ import { TldrawUiDropdownMenuItem } from '../TldrawUiDropdownMenu'
24
24
  import { TLUiIconJsx } from '../TldrawUiIcon'
25
25
  import { TldrawUiKbd } from '../TldrawUiKbd'
26
26
  import { TldrawUiToolbarButton } from '../TldrawUiToolbar'
27
- import { tooltipManager } from '../TldrawUiTooltip'
27
+ import { hideAllTooltips } from '../TldrawUiTooltip'
28
28
  import { useTldrawUiMenuContext } from './TldrawUiMenuContext'
29
29
 
30
30
  /** @public */
@@ -350,7 +350,7 @@ function useDraggableEvents(
350
350
  point: screenSpaceStart,
351
351
  })
352
352
 
353
- tooltipManager.hideAllTooltips()
353
+ hideAllTooltips()
354
354
  editor.getContainer().focus()
355
355
  })
356
356
  }