tldraw 5.4.0-next.ef99ab47e5ce → 5.4.0

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 (316) hide show
  1. package/DOCS.md +2826 -3226
  2. package/RELEASE_NOTES.md +190 -2
  3. package/dist-cjs/index.d.ts +264 -275
  4. package/dist-cjs/index.js +1 -1
  5. package/dist-cjs/lib/defaultExternalContentHandlers.js +32 -16
  6. package/dist-cjs/lib/defaultExternalContentHandlers.js.map +2 -2
  7. package/dist-cjs/lib/shapes/arrow/curved-arrow.js +2 -2
  8. package/dist-cjs/lib/shapes/arrow/curved-arrow.js.map +2 -2
  9. package/dist-cjs/lib/shapes/arrow/elbow/getElbowArrowInfo.js +2 -2
  10. package/dist-cjs/lib/shapes/arrow/elbow/getElbowArrowInfo.js.map +2 -2
  11. package/dist-cjs/lib/shapes/arrow/getArrowInfo.js +1 -1
  12. package/dist-cjs/lib/shapes/arrow/getArrowInfo.js.map +2 -2
  13. package/dist-cjs/lib/shapes/arrow/straight-arrow.js +2 -2
  14. package/dist-cjs/lib/shapes/arrow/straight-arrow.js.map +2 -2
  15. package/dist-cjs/lib/shapes/arrow/toolStates/Idle.js +2 -3
  16. package/dist-cjs/lib/shapes/arrow/toolStates/Idle.js.map +2 -2
  17. package/dist-cjs/lib/shapes/arrow/toolStates/Pointing.js +7 -5
  18. package/dist-cjs/lib/shapes/arrow/toolStates/Pointing.js.map +2 -2
  19. package/dist-cjs/lib/shapes/draw/toolStates/Drawing.js +8 -6
  20. package/dist-cjs/lib/shapes/draw/toolStates/Drawing.js.map +2 -2
  21. package/dist-cjs/lib/shapes/geo/getGeoShapePath.js +3 -1
  22. package/dist-cjs/lib/shapes/geo/getGeoShapePath.js.map +2 -2
  23. package/dist-cjs/lib/shapes/geo/toolStates/Idle.js +2 -3
  24. package/dist-cjs/lib/shapes/geo/toolStates/Idle.js.map +2 -2
  25. package/dist-cjs/lib/shapes/geo/toolStates/Pointing.js +1 -1
  26. package/dist-cjs/lib/shapes/geo/toolStates/Pointing.js.map +2 -2
  27. package/dist-cjs/lib/shapes/line/toolStates/Pointing.js +2 -6
  28. package/dist-cjs/lib/shapes/line/toolStates/Pointing.js.map +2 -2
  29. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js +4 -4
  30. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js.map +2 -2
  31. package/dist-cjs/lib/shapes/note/noteHelpers.js +6 -5
  32. package/dist-cjs/lib/shapes/note/noteHelpers.js.map +2 -2
  33. package/dist-cjs/lib/shapes/note/toolStates/Pointing.js +1 -1
  34. package/dist-cjs/lib/shapes/note/toolStates/Pointing.js.map +2 -2
  35. package/dist-cjs/lib/shapes/shared/crop.js +1 -1
  36. package/dist-cjs/lib/shapes/shared/crop.js.map +2 -2
  37. package/dist-cjs/lib/shapes/text/toolStates/Idle.js +1 -1
  38. package/dist-cjs/lib/shapes/text/toolStates/Idle.js.map +2 -2
  39. package/dist-cjs/lib/shapes/text/toolStates/Pointing.js +1 -0
  40. package/dist-cjs/lib/shapes/text/toolStates/Pointing.js.map +2 -2
  41. package/dist-cjs/lib/shapes/video/VideoShapeUtil.js +3 -2
  42. package/dist-cjs/lib/shapes/video/VideoShapeUtil.js.map +2 -2
  43. package/dist-cjs/lib/tools/EraserTool/childStates/Erasing.js +2 -0
  44. package/dist-cjs/lib/tools/EraserTool/childStates/Erasing.js.map +2 -2
  45. package/dist-cjs/lib/tools/HandTool/childStates/OneFingerZooming.js +6 -0
  46. package/dist-cjs/lib/tools/HandTool/childStates/OneFingerZooming.js.map +2 -2
  47. package/dist-cjs/lib/tools/SelectTool/SelectTool.js +3 -0
  48. package/dist-cjs/lib/tools/SelectTool/SelectTool.js.map +2 -2
  49. package/dist-cjs/lib/tools/SelectTool/childStates/Brushing.js +1 -0
  50. package/dist-cjs/lib/tools/SelectTool/childStates/Brushing.js.map +2 -2
  51. package/dist-cjs/lib/tools/SelectTool/childStates/Crop/Crop.js +1 -0
  52. package/dist-cjs/lib/tools/SelectTool/childStates/Crop/Crop.js.map +2 -2
  53. package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js +1 -3
  54. package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js.map +2 -2
  55. package/dist-cjs/lib/tools/SelectTool/childStates/Idle.js +6 -3
  56. package/dist-cjs/lib/tools/SelectTool/childStates/Idle.js.map +2 -2
  57. package/dist-cjs/lib/tools/SelectTool/childStates/PointingArrowLabel.js +1 -1
  58. package/dist-cjs/lib/tools/SelectTool/childStates/PointingArrowLabel.js.map +2 -2
  59. package/dist-cjs/lib/tools/SelectTool/childStates/PointingCanvas.js +3 -0
  60. package/dist-cjs/lib/tools/SelectTool/childStates/PointingCanvas.js.map +2 -2
  61. package/dist-cjs/lib/tools/SelectTool/childStates/PointingHandle.js +1 -0
  62. package/dist-cjs/lib/tools/SelectTool/childStates/PointingHandle.js.map +2 -2
  63. package/dist-cjs/lib/tools/SelectTool/childStates/PointingShape.js +1 -1
  64. package/dist-cjs/lib/tools/SelectTool/childStates/PointingShape.js.map +2 -2
  65. package/dist-cjs/lib/tools/SelectTool/childStates/Resizing.js +22 -30
  66. package/dist-cjs/lib/tools/SelectTool/childStates/Resizing.js.map +2 -2
  67. package/dist-cjs/lib/tools/SelectTool/childStates/ScribbleBrushing.js +1 -3
  68. package/dist-cjs/lib/tools/SelectTool/childStates/ScribbleBrushing.js.map +2 -2
  69. package/dist-cjs/lib/tools/SelectTool/childStates/Translating.js +4 -1
  70. package/dist-cjs/lib/tools/SelectTool/childStates/Translating.js.map +2 -2
  71. package/dist-cjs/lib/tools/SelectTool/selectHelpers.js +10 -0
  72. package/dist-cjs/lib/tools/SelectTool/selectHelpers.js.map +2 -2
  73. package/dist-cjs/lib/ui/TldrawUi.js +2 -3
  74. package/dist-cjs/lib/ui/TldrawUi.js.map +2 -2
  75. package/dist-cjs/lib/ui/components/ActionsMenu/DefaultActionsMenu.js +1 -11
  76. package/dist-cjs/lib/ui/components/ActionsMenu/DefaultActionsMenu.js.map +2 -2
  77. package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenu.js +6 -1
  78. package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenu.js.map +2 -2
  79. package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenuContent.js +1 -1
  80. package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenuContent.js.map +2 -2
  81. package/dist-cjs/lib/ui/components/Dialogs.js +8 -0
  82. package/dist-cjs/lib/ui/components/Dialogs.js.map +2 -2
  83. package/dist-cjs/lib/ui/components/EditLinkDialog.js +5 -5
  84. package/dist-cjs/lib/ui/components/EditLinkDialog.js.map +2 -2
  85. package/dist-cjs/lib/ui/components/Minimap/DefaultMinimap.js +4 -20
  86. package/dist-cjs/lib/ui/components/Minimap/DefaultMinimap.js.map +2 -2
  87. package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuItem.js +1 -2
  88. package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuItem.js.map +2 -2
  89. package/dist-cjs/lib/ui/components/StylePanel/StylePanelDoubleDropdownPicker.js +2 -0
  90. package/dist-cjs/lib/ui/components/StylePanel/StylePanelDoubleDropdownPicker.js.map +2 -2
  91. package/dist-cjs/lib/ui/components/Toolbar/AltTextEditor.js +1 -0
  92. package/dist-cjs/lib/ui/components/Toolbar/AltTextEditor.js.map +2 -2
  93. package/dist-cjs/lib/ui/components/Toolbar/DefaultImageToolbarContent.js +10 -8
  94. package/dist-cjs/lib/ui/components/Toolbar/DefaultImageToolbarContent.js.map +2 -2
  95. package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js +20 -1
  96. package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js.map +2 -2
  97. package/dist-cjs/lib/ui/context/actions.js +8 -8
  98. package/dist-cjs/lib/ui/context/actions.js.map +2 -2
  99. package/dist-cjs/lib/ui/hooks/useClipboardEvents.js +2 -0
  100. package/dist-cjs/lib/ui/hooks/useClipboardEvents.js.map +2 -2
  101. package/dist-cjs/lib/ui/hooks/useCommentingEnabled.js +1 -1
  102. package/dist-cjs/lib/ui/hooks/useCommentingEnabled.js.map +2 -2
  103. package/dist-cjs/lib/ui/hooks/useFlatten.js +42 -23
  104. package/dist-cjs/lib/ui/hooks/useFlatten.js.map +2 -2
  105. package/dist-cjs/lib/ui/version.js +3 -3
  106. package/dist-cjs/lib/ui/version.js.map +1 -1
  107. package/dist-esm/index.d.mts +264 -275
  108. package/dist-esm/index.mjs +1 -1
  109. package/dist-esm/lib/defaultExternalContentHandlers.mjs +32 -16
  110. package/dist-esm/lib/defaultExternalContentHandlers.mjs.map +2 -2
  111. package/dist-esm/lib/shapes/arrow/curved-arrow.mjs +2 -2
  112. package/dist-esm/lib/shapes/arrow/curved-arrow.mjs.map +2 -2
  113. package/dist-esm/lib/shapes/arrow/elbow/getElbowArrowInfo.mjs +2 -2
  114. package/dist-esm/lib/shapes/arrow/elbow/getElbowArrowInfo.mjs.map +2 -2
  115. package/dist-esm/lib/shapes/arrow/getArrowInfo.mjs +1 -1
  116. package/dist-esm/lib/shapes/arrow/getArrowInfo.mjs.map +2 -2
  117. package/dist-esm/lib/shapes/arrow/straight-arrow.mjs +2 -2
  118. package/dist-esm/lib/shapes/arrow/straight-arrow.mjs.map +2 -2
  119. package/dist-esm/lib/shapes/arrow/toolStates/Idle.mjs +3 -4
  120. package/dist-esm/lib/shapes/arrow/toolStates/Idle.mjs.map +2 -2
  121. package/dist-esm/lib/shapes/arrow/toolStates/Pointing.mjs +7 -5
  122. package/dist-esm/lib/shapes/arrow/toolStates/Pointing.mjs.map +2 -2
  123. package/dist-esm/lib/shapes/draw/toolStates/Drawing.mjs +8 -6
  124. package/dist-esm/lib/shapes/draw/toolStates/Drawing.mjs.map +2 -2
  125. package/dist-esm/lib/shapes/geo/getGeoShapePath.mjs +3 -1
  126. package/dist-esm/lib/shapes/geo/getGeoShapePath.mjs.map +2 -2
  127. package/dist-esm/lib/shapes/geo/toolStates/Idle.mjs +3 -4
  128. package/dist-esm/lib/shapes/geo/toolStates/Idle.mjs.map +2 -2
  129. package/dist-esm/lib/shapes/geo/toolStates/Pointing.mjs +1 -1
  130. package/dist-esm/lib/shapes/geo/toolStates/Pointing.mjs.map +2 -2
  131. package/dist-esm/lib/shapes/line/toolStates/Pointing.mjs +2 -7
  132. package/dist-esm/lib/shapes/line/toolStates/Pointing.mjs.map +2 -2
  133. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs +4 -4
  134. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs.map +2 -2
  135. package/dist-esm/lib/shapes/note/noteHelpers.mjs +6 -5
  136. package/dist-esm/lib/shapes/note/noteHelpers.mjs.map +2 -2
  137. package/dist-esm/lib/shapes/note/toolStates/Pointing.mjs +1 -1
  138. package/dist-esm/lib/shapes/note/toolStates/Pointing.mjs.map +2 -2
  139. package/dist-esm/lib/shapes/shared/crop.mjs +1 -1
  140. package/dist-esm/lib/shapes/shared/crop.mjs.map +2 -2
  141. package/dist-esm/lib/shapes/text/toolStates/Idle.mjs +2 -2
  142. package/dist-esm/lib/shapes/text/toolStates/Idle.mjs.map +2 -2
  143. package/dist-esm/lib/shapes/text/toolStates/Pointing.mjs +1 -0
  144. package/dist-esm/lib/shapes/text/toolStates/Pointing.mjs.map +2 -2
  145. package/dist-esm/lib/shapes/video/VideoShapeUtil.mjs +3 -2
  146. package/dist-esm/lib/shapes/video/VideoShapeUtil.mjs.map +2 -2
  147. package/dist-esm/lib/tools/EraserTool/childStates/Erasing.mjs +2 -0
  148. package/dist-esm/lib/tools/EraserTool/childStates/Erasing.mjs.map +2 -2
  149. package/dist-esm/lib/tools/HandTool/childStates/OneFingerZooming.mjs +6 -0
  150. package/dist-esm/lib/tools/HandTool/childStates/OneFingerZooming.mjs.map +2 -2
  151. package/dist-esm/lib/tools/SelectTool/SelectTool.mjs +3 -0
  152. package/dist-esm/lib/tools/SelectTool/SelectTool.mjs.map +2 -2
  153. package/dist-esm/lib/tools/SelectTool/childStates/Brushing.mjs +1 -0
  154. package/dist-esm/lib/tools/SelectTool/childStates/Brushing.mjs.map +2 -2
  155. package/dist-esm/lib/tools/SelectTool/childStates/Crop/Crop.mjs +1 -0
  156. package/dist-esm/lib/tools/SelectTool/childStates/Crop/Crop.mjs.map +2 -2
  157. package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs +1 -3
  158. package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs.map +2 -2
  159. package/dist-esm/lib/tools/SelectTool/childStates/Idle.mjs +6 -3
  160. package/dist-esm/lib/tools/SelectTool/childStates/Idle.mjs.map +2 -2
  161. package/dist-esm/lib/tools/SelectTool/childStates/PointingArrowLabel.mjs +1 -1
  162. package/dist-esm/lib/tools/SelectTool/childStates/PointingArrowLabel.mjs.map +2 -2
  163. package/dist-esm/lib/tools/SelectTool/childStates/PointingCanvas.mjs +3 -0
  164. package/dist-esm/lib/tools/SelectTool/childStates/PointingCanvas.mjs.map +2 -2
  165. package/dist-esm/lib/tools/SelectTool/childStates/PointingHandle.mjs +5 -1
  166. package/dist-esm/lib/tools/SelectTool/childStates/PointingHandle.mjs.map +2 -2
  167. package/dist-esm/lib/tools/SelectTool/childStates/PointingShape.mjs +1 -1
  168. package/dist-esm/lib/tools/SelectTool/childStates/PointingShape.mjs.map +2 -2
  169. package/dist-esm/lib/tools/SelectTool/childStates/Resizing.mjs +22 -31
  170. package/dist-esm/lib/tools/SelectTool/childStates/Resizing.mjs.map +2 -2
  171. package/dist-esm/lib/tools/SelectTool/childStates/ScribbleBrushing.mjs +1 -3
  172. package/dist-esm/lib/tools/SelectTool/childStates/ScribbleBrushing.mjs.map +2 -2
  173. package/dist-esm/lib/tools/SelectTool/childStates/Translating.mjs +4 -1
  174. package/dist-esm/lib/tools/SelectTool/childStates/Translating.mjs.map +2 -2
  175. package/dist-esm/lib/tools/SelectTool/selectHelpers.mjs +10 -0
  176. package/dist-esm/lib/tools/SelectTool/selectHelpers.mjs.map +2 -2
  177. package/dist-esm/lib/ui/TldrawUi.mjs +2 -3
  178. package/dist-esm/lib/ui/TldrawUi.mjs.map +2 -2
  179. package/dist-esm/lib/ui/components/ActionsMenu/DefaultActionsMenu.mjs +2 -12
  180. package/dist-esm/lib/ui/components/ActionsMenu/DefaultActionsMenu.mjs.map +2 -2
  181. package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenu.mjs +7 -2
  182. package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenu.mjs.map +2 -2
  183. package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenuContent.mjs +1 -1
  184. package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenuContent.mjs.map +2 -2
  185. package/dist-esm/lib/ui/components/Dialogs.mjs +9 -1
  186. package/dist-esm/lib/ui/components/Dialogs.mjs.map +2 -2
  187. package/dist-esm/lib/ui/components/EditLinkDialog.mjs +5 -5
  188. package/dist-esm/lib/ui/components/EditLinkDialog.mjs.map +2 -2
  189. package/dist-esm/lib/ui/components/Minimap/DefaultMinimap.mjs +5 -22
  190. package/dist-esm/lib/ui/components/Minimap/DefaultMinimap.mjs.map +2 -2
  191. package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuItem.mjs +1 -2
  192. package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuItem.mjs.map +2 -2
  193. package/dist-esm/lib/ui/components/StylePanel/StylePanelDoubleDropdownPicker.mjs +2 -0
  194. package/dist-esm/lib/ui/components/StylePanel/StylePanelDoubleDropdownPicker.mjs.map +2 -2
  195. package/dist-esm/lib/ui/components/Toolbar/AltTextEditor.mjs +1 -0
  196. package/dist-esm/lib/ui/components/Toolbar/AltTextEditor.mjs.map +2 -2
  197. package/dist-esm/lib/ui/components/Toolbar/DefaultImageToolbarContent.mjs +10 -8
  198. package/dist-esm/lib/ui/components/Toolbar/DefaultImageToolbarContent.mjs.map +2 -2
  199. package/dist-esm/lib/ui/components/primitives/TldrawUiSlider.mjs +20 -1
  200. package/dist-esm/lib/ui/components/primitives/TldrawUiSlider.mjs.map +2 -2
  201. package/dist-esm/lib/ui/context/actions.mjs +8 -8
  202. package/dist-esm/lib/ui/context/actions.mjs.map +2 -2
  203. package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs +2 -0
  204. package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs.map +2 -2
  205. package/dist-esm/lib/ui/hooks/useCommentingEnabled.mjs +2 -2
  206. package/dist-esm/lib/ui/hooks/useCommentingEnabled.mjs.map +2 -2
  207. package/dist-esm/lib/ui/hooks/useFlatten.mjs +42 -23
  208. package/dist-esm/lib/ui/hooks/useFlatten.mjs.map +2 -2
  209. package/dist-esm/lib/ui/version.mjs +3 -3
  210. package/dist-esm/lib/ui/version.mjs.map +1 -1
  211. package/package.json +12 -6
  212. package/src/lib/defaultExternalContentHandlers.test.ts +43 -0
  213. package/src/lib/defaultExternalContentHandlers.ts +40 -18
  214. package/src/lib/shapes/arrow/ArrowShapeTool.test.ts +17 -0
  215. package/src/lib/shapes/arrow/curved-arrow.ts +2 -2
  216. package/src/lib/shapes/arrow/elbow/getElbowArrowInfo.tsx +5 -3
  217. package/src/lib/shapes/arrow/getArrowInfo.ts +2 -1
  218. package/src/lib/shapes/arrow/straight-arrow.ts +2 -2
  219. package/src/lib/shapes/arrow/toolStates/Idle.tsx +3 -4
  220. package/src/lib/shapes/arrow/toolStates/Pointing.tsx +10 -6
  221. package/src/lib/shapes/draw/DrawShapeTool.test.ts +19 -0
  222. package/src/lib/shapes/draw/toolStates/Drawing.ts +12 -6
  223. package/src/lib/shapes/frame/FrameShapeTool.test.ts +17 -0
  224. package/src/lib/shapes/geo/GeoShapeTool.test.ts +28 -0
  225. package/src/lib/shapes/geo/getGeoShapePath.ts +3 -1
  226. package/src/lib/shapes/geo/toolStates/Idle.ts +3 -4
  227. package/src/lib/shapes/geo/toolStates/Pointing.ts +2 -1
  228. package/src/lib/shapes/line/LineShapeTool.test.ts +30 -1
  229. package/src/lib/shapes/line/toolStates/Pointing.ts +6 -7
  230. package/src/lib/shapes/note/NoteShapeTool.test.ts +45 -0
  231. package/src/lib/shapes/note/NoteShapeUtil.tsx +7 -4
  232. package/src/lib/shapes/note/noteHelpers.ts +18 -8
  233. package/src/lib/shapes/note/toolStates/Pointing.ts +2 -1
  234. package/src/lib/shapes/shared/crop.ts +7 -1
  235. package/src/lib/shapes/text/TextShapeTool.test.ts +33 -0
  236. package/src/lib/shapes/text/toolStates/Idle.ts +2 -2
  237. package/src/lib/shapes/text/toolStates/Pointing.ts +3 -0
  238. package/src/lib/shapes/video/VideoShapeUtil.tsx +4 -2
  239. package/src/lib/tools/EraserTool/childStates/Erasing.ts +5 -0
  240. package/src/lib/tools/HandTool/childStates/OneFingerZooming.ts +10 -0
  241. package/src/lib/tools/SelectTool/SelectTool.ts +4 -0
  242. package/src/lib/tools/SelectTool/childStates/Brushing.ts +4 -0
  243. package/src/lib/tools/SelectTool/childStates/Crop/Crop.ts +4 -0
  244. package/src/lib/tools/SelectTool/childStates/DraggingHandle.tsx +3 -4
  245. package/src/lib/tools/SelectTool/childStates/Idle.ts +10 -3
  246. package/src/lib/tools/SelectTool/childStates/PointingArrowLabel.ts +1 -1
  247. package/src/lib/tools/SelectTool/childStates/PointingCanvas.ts +4 -0
  248. package/src/lib/tools/SelectTool/childStates/PointingHandle.ts +6 -1
  249. package/src/lib/tools/SelectTool/childStates/PointingShape.ts +4 -1
  250. package/src/lib/tools/SelectTool/childStates/Resizing.ts +30 -33
  251. package/src/lib/tools/SelectTool/childStates/ScribbleBrushing.ts +3 -3
  252. package/src/lib/tools/SelectTool/childStates/Translating.ts +6 -0
  253. package/src/lib/tools/SelectTool/selectHelpers.ts +21 -0
  254. package/src/lib/ui/TldrawUi.tsx +1 -2
  255. package/src/lib/ui/components/ActionsMenu/DefaultActionsMenu.tsx +4 -14
  256. package/src/lib/ui/components/ContextMenu/DefaultContextMenu.tsx +8 -2
  257. package/src/lib/ui/components/ContextMenu/DefaultContextMenuContent.tsx +5 -1
  258. package/src/lib/ui/components/Dialogs.tsx +17 -1
  259. package/src/lib/ui/components/EditLinkDialog.tsx +8 -7
  260. package/src/lib/ui/components/Minimap/DefaultMinimap.tsx +5 -23
  261. package/src/lib/ui/components/SharePanel/DefaultPeopleMenuItem.tsx +2 -3
  262. package/src/lib/ui/components/StylePanel/StylePanelDoubleDropdownPicker.tsx +2 -0
  263. package/src/lib/ui/components/Toolbar/AltTextEditor.tsx +1 -0
  264. package/src/lib/ui/components/Toolbar/DefaultImageToolbarContent.tsx +13 -8
  265. package/src/lib/ui/components/primitives/TldrawUiSlider.test.tsx +92 -0
  266. package/src/lib/ui/components/primitives/TldrawUiSlider.tsx +25 -1
  267. package/src/lib/ui/context/actions.tsx +25 -8
  268. package/src/lib/ui/hooks/useClipboardEvents.ts +2 -0
  269. package/src/lib/ui/hooks/useCommentingEnabled.ts +5 -3
  270. package/src/lib/ui/hooks/useFlatten.ts +56 -23
  271. package/src/lib/ui/version.ts +3 -3
  272. package/src/lib/ui.css +7 -16
  273. package/src/test/ClickManager.test.ts +18 -0
  274. package/src/test/Editor.test.tsx +3 -0
  275. package/src/test/EraserTool.test.ts +18 -2
  276. package/src/test/HandTool.test.ts +21 -0
  277. package/src/test/LaserTool.test.ts +46 -0
  278. package/src/test/ScribbleManager.test.ts +19 -0
  279. package/src/test/SelectTool.test.ts +110 -0
  280. package/src/test/TldrawEditor.test.tsx +18 -0
  281. package/src/test/VideoShapeUtil.test.tsx +126 -0
  282. package/src/test/actions-menu-readonly.test.tsx +37 -0
  283. package/src/test/bookmark-shapes.test.ts +110 -0
  284. package/src/test/commands/__snapshots__/packShapes.test.ts.snap +6 -6
  285. package/src/test/commands/clipboard.test.ts +36 -1
  286. package/src/test/commands/lockShapes.test.ts +11 -0
  287. package/src/test/commands/packShapes.test.ts +20 -0
  288. package/src/test/commands/putContent.test.ts +35 -1
  289. package/src/test/commands/setCamera.test.ts +69 -0
  290. package/src/test/commands/stackShapes.test.ts +12 -0
  291. package/src/test/commands/updateViewportPageBounds.test.ts +8 -0
  292. package/src/test/cropping.test.ts +162 -0
  293. package/src/test/customSnapping.test.tsx +32 -0
  294. package/src/test/defaultHandleExternalFileContent.test.ts +86 -0
  295. package/src/test/drawing.test.ts +60 -1
  296. package/src/test/duplicate.test.ts +22 -0
  297. package/src/test/flatten.test.ts +135 -0
  298. package/src/test/frames.test.ts +51 -0
  299. package/src/test/getShapeAtPoint.test.ts +44 -1
  300. package/src/test/handleDeepLink.test.tsx +15 -0
  301. package/src/test/minimap-wheel.test.tsx +48 -0
  302. package/src/test/resizing.test.ts +46 -0
  303. package/src/test/selection-omnibus.test.ts +50 -0
  304. package/src/test/spacebarPanning.test.ts +58 -0
  305. package/src/test/translating.test.ts +42 -0
  306. package/src/test/ui/AltTextEditor.test.tsx +51 -0
  307. package/src/test/ui/ContextMenu.test.tsx +61 -2
  308. package/src/test/ui/EditLinkDialog.test.tsx +77 -0
  309. package/src/test/ui/ImageToolbar.test.tsx +58 -0
  310. package/src/test/ui/StylePanel.test.tsx +33 -2
  311. package/src/test/ui/keyboardShortcuts.test.tsx +27 -0
  312. package/src/test/ui/presenceActivity.test.tsx +44 -0
  313. package/src/test/ui/unlockAll.test.tsx +55 -0
  314. package/src/test/ui-outside-editor.test.tsx +74 -0
  315. package/src/test/viewport-following.test.ts +29 -1
  316. package/tldraw.css +7 -16
@@ -80,3 +80,61 @@ it('When holding spacebar, pressing the arrow keys moves over by one viewport',
80
80
  expect(editor.getViewportPageBounds()).toEqual({ x: 1080, y: 720, w: 1080, h: 720 })
81
81
  editor.keyUp(' ')
82
82
  })
83
+
84
+ describe('When cancelling while spacebar is held', () => {
85
+ it('keeps panning through a cancel while the pointer is down', () => {
86
+ editor.keyDown(' ')
87
+ editor.pointerDown(0, 0)
88
+ editor.pointerMove(50, 50)
89
+ editor.expectCameraToBe(50, 50, 1)
90
+
91
+ editor.cancel()
92
+ expect(editor.inputs.getIsPanning()).toBe(true)
93
+ expect(editor.inputs.getIsSpacebarPanning()).toBe(true)
94
+ expect(editor.getInstanceState().cursor.type).toBe('grabbing')
95
+
96
+ editor.pointerMove(100, 100)
97
+ editor.expectCameraToBe(100, 100, 1)
98
+
99
+ editor.pointerUp(100, 100)
100
+ editor.keyUp(' ')
101
+ expect(editor.inputs.getIsPanning()).toBe(false)
102
+ expect(editor.getInstanceState().cursor.type).toBe('default')
103
+ })
104
+
105
+ it('keeps panning through a cancel while the pointer is up', () => {
106
+ editor.keyDown(' ')
107
+ editor.cancel()
108
+ expect(editor.inputs.getIsPanning()).toBe(true)
109
+ expect(editor.getInstanceState().cursor.type).toBe('grab')
110
+
111
+ editor.pointerDown(0, 0)
112
+ editor.pointerMove(100, 100)
113
+ editor.expectCameraToBe(100, 100, 1)
114
+ editor.pointerUp(100, 100)
115
+ editor.keyUp(' ')
116
+ expect(editor.inputs.getIsPanning()).toBe(false)
117
+ })
118
+
119
+ it('keeps panning through a complete while spacebar is held', () => {
120
+ editor.keyDown(' ')
121
+ editor.pointerDown(0, 0)
122
+ editor.complete()
123
+ expect(editor.inputs.getIsPanning()).toBe(true)
124
+ expect(editor.getInstanceState().cursor.type).toBe('grabbing')
125
+
126
+ editor.pointerMove(100, 100)
127
+ editor.expectCameraToBe(100, 100, 1)
128
+ editor.pointerUp(100, 100)
129
+ editor.keyUp(' ')
130
+ expect(editor.inputs.getIsPanning()).toBe(false)
131
+ })
132
+
133
+ it('still ends a middle mouse pan on cancel when spacebar is not held', () => {
134
+ editor.pointerDown(0, 0, { button: 1 })
135
+ expect(editor.inputs.getIsPanning()).toBe(true)
136
+ editor.cancel()
137
+ expect(editor.inputs.getIsPanning()).toBe(false)
138
+ expect(editor.getInstanceState().cursor.type).toBe('default')
139
+ })
140
+ })
@@ -2305,6 +2305,48 @@ describe('cancelling a translate operation', () => {
2305
2305
  })
2306
2306
  })
2307
2307
 
2308
+ describe('cloning mid-drag', () => {
2309
+ it('reparents the clone, not the original, when alt is pressed during the drag', () => {
2310
+ editor.createShapes([
2311
+ { id: ids.frame1, type: 'frame', x: 500, y: 0, props: { w: 200, h: 200 } },
2312
+ { id: ids.box1, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100 } },
2313
+ ])
2314
+ editor.pointerDown(50, 50, { target: 'shape', shape: editor.getShape(ids.box1) })
2315
+ editor.pointerMove(60, 60)
2316
+ editor.expectToBeIn('select.translating')
2317
+ editor.keyDown('Alt')
2318
+ editor.pointerMove(600, 100)
2319
+ vi.advanceTimersByTime(300)
2320
+ editor.pointerUp(600, 100)
2321
+
2322
+ const original = editor.getShape(ids.box1)!
2323
+ expect(original.parentId).toBe(editor.getCurrentPageId())
2324
+ expect(editor.getShapePageBounds(original)).toMatchObject({ x: 0, y: 0 })
2325
+
2326
+ const clone = editor.getCurrentPageShapes().find((s) => s.type === 'geo' && s.id !== ids.box1)!
2327
+ expect(clone.parentId).toBe(ids.frame1)
2328
+ })
2329
+
2330
+ it('reparents the original when alt is released during the drag', () => {
2331
+ editor.createShapes([
2332
+ { id: ids.frame1, type: 'frame', x: 500, y: 0, props: { w: 200, h: 200 } },
2333
+ { id: ids.box1, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100 } },
2334
+ ])
2335
+ editor.keyDown('Alt')
2336
+ editor.pointerDown(50, 50, { target: 'shape', shape: editor.getShape(ids.box1) })
2337
+ editor.pointerMove(60, 60)
2338
+ editor.expectToBeIn('select.translating')
2339
+ editor.keyUp('Alt')
2340
+ vi.advanceTimersByTime(250) // the alt key is released on a timer
2341
+ editor.pointerMove(600, 100)
2342
+ vi.advanceTimersByTime(300)
2343
+ editor.pointerUp(600, 100)
2344
+
2345
+ expect(editor.getCurrentPageShapes().filter((s) => s.type === 'geo')).toHaveLength(1)
2346
+ expect(editor.getShape(ids.box1)!.parentId).toBe(ids.frame1)
2347
+ })
2348
+ })
2349
+
2308
2350
  it('preserves z-indexes when translating', () => {
2309
2351
  editor.createShape({ type: 'geo', x: 0, y: 0, props: { w: 200, h: 200 } })
2310
2352
  editor.createShape({ type: 'geo', x: 100, y: 100, props: { w: 200, h: 200 } })
@@ -0,0 +1,51 @@
1
+ import { act, fireEvent, screen } from '@testing-library/react'
2
+ import { createShapeId, Editor, TLImageShape } from '@tldraw/editor'
3
+ import { Tldraw } from '../../lib/Tldraw'
4
+ import { renderTldrawComponentWithEditor } from '../testutils/renderTldrawComponent'
5
+
6
+ let editor: Editor
7
+ const imageId = createShapeId('image')
8
+
9
+ beforeEach(async () => {
10
+ const result = await renderTldrawComponentWithEditor((onMount) => <Tldraw onMount={onMount} />, {
11
+ waitForPatterns: false,
12
+ })
13
+ editor = result.editor
14
+
15
+ act(() => {
16
+ editor.createShape<TLImageShape>({
17
+ id: imageId,
18
+ type: 'image',
19
+ x: 100,
20
+ y: 100,
21
+ props: { w: 200, h: 200 },
22
+ })
23
+ editor.markHistoryStoppingPoint('move image')
24
+ editor.updateShape({ id: imageId, type: 'image', x: 300 })
25
+ editor.select(imageId)
26
+ })
27
+ })
28
+
29
+ describe('AltTextEditor', () => {
30
+ it('undoes only the alt text change, not the action before it', async () => {
31
+ fireEvent.click(await screen.findByTestId('tool.image-alt-text'))
32
+
33
+ const input = (await screen.findByTestId('media-toolbar.alt-text-input')) as HTMLInputElement
34
+ fireEvent.change(input, { target: { value: 'A picture' } })
35
+ fireEvent.keyDown(input, { key: 'Enter' })
36
+
37
+ expect(editor.getShape<TLImageShape>(imageId)).toMatchObject({
38
+ x: 300,
39
+ props: { altText: 'A picture' },
40
+ })
41
+
42
+ act(() => {
43
+ editor.undo()
44
+ })
45
+
46
+ expect(editor.getShape<TLImageShape>(imageId)).toMatchObject({
47
+ x: 300,
48
+ props: { altText: '' },
49
+ })
50
+ })
51
+ })
@@ -1,5 +1,6 @@
1
- import { act, fireEvent, screen } from '@testing-library/react'
2
- import { createShapeId, Editor, tlenvReactive } from '@tldraw/editor'
1
+ import { act, fireEvent, screen, waitFor } from '@testing-library/react'
2
+ import { createShapeId, Editor, TldrawOptions, tlenvReactive } from '@tldraw/editor'
3
+ import { vi } from 'vitest'
3
4
  import { TLComponents, Tldraw } from '../../lib/Tldraw'
4
5
  import { DefaultContextMenu } from '../../lib/ui/components/ContextMenu/DefaultContextMenu'
5
6
  import { TLUiOverrides } from '../../lib/ui/overrides'
@@ -8,6 +9,16 @@ import {
8
9
  renderTldrawComponentWithEditor,
9
10
  } from '../testutils/renderTldrawComponent'
10
11
 
12
+ // The paste item only renders when the clipboard API exists at module load, so the
13
+ // stub has to be in place before the menu modules are imported.
14
+ const clipboard = vi.hoisted(() => {
15
+ const clipboard = { read: vi.fn(), write: vi.fn() }
16
+ Object.assign(navigator, { clipboard })
17
+ // @ts-expect-error jsdom has no ClipboardItem
18
+ window.ClipboardItem = class {}
19
+ return clipboard
20
+ })
21
+
11
22
  it('opens on right-click', async () => {
12
23
  await renderTldrawComponent(
13
24
  <Tldraw
@@ -167,6 +178,54 @@ it('right-click in a shape tool reveals shape actions (rightClickPanning off)',
167
178
  await screen.findByTestId('context-menu.delete')
168
179
  })
169
180
 
181
+ it('pastes at the point where the menu was opened, not where the item was chosen', async () => {
182
+ // Regression for #10423: the pointer keeps moving (over the menu) between the
183
+ // right-click and choosing Paste, and the pasted content used to follow it.
184
+ const onClipboardPasteRaw = vi.fn<NonNullable<TldrawOptions['onClipboardPasteRaw']>>(() => false)
185
+ const { editor } = await renderTldrawComponentWithEditor(
186
+ (onMount) => <Tldraw options={{ onClipboardPasteRaw }} onMount={onMount} />,
187
+ { waitForPatterns: false }
188
+ )
189
+ clipboard.read.mockResolvedValue([new window.ClipboardItem({})])
190
+
191
+ function movePointerTo(x: number, y: number) {
192
+ act(() => {
193
+ editor.dispatch({
194
+ type: 'pointer',
195
+ name: 'pointer_move',
196
+ target: 'canvas',
197
+ point: { x, y, z: 0.5 },
198
+ pointerId: 1,
199
+ button: 0,
200
+ isPen: false,
201
+ shiftKey: false,
202
+ altKey: false,
203
+ ctrlKey: false,
204
+ metaKey: false,
205
+ accelKey: false,
206
+ })
207
+ // pointer moves are batched until the next tick
208
+ editor.emit('tick', 16)
209
+ })
210
+ }
211
+
212
+ const canvas = await screen.findByTestId('canvas')
213
+ movePointerTo(100, 200)
214
+ fireEvent.contextMenu(canvas)
215
+ const pasteItem = await screen.findByTestId('context-menu.paste')
216
+
217
+ // travel down the menu before choosing the item
218
+ movePointerTo(300, 400)
219
+ expect(editor.inputs.getCurrentPagePoint()).toMatchObject({ x: 300, y: 400 })
220
+
221
+ fireEvent.click(pasteItem)
222
+ await waitFor(() => expect(onClipboardPasteRaw).toHaveBeenCalled())
223
+ expect(onClipboardPasteRaw.mock.calls[0][0]).toMatchObject({
224
+ source: 'clipboard-read',
225
+ point: { x: 100, y: 200 },
226
+ })
227
+ })
228
+
170
229
  it('tunnels context menu', async () => {
171
230
  const components: TLComponents = {
172
231
  ContextMenu: (props) => {
@@ -0,0 +1,77 @@
1
+ import { act, fireEvent, screen, waitFor } from '@testing-library/react'
2
+ import { createShapeId, Editor } from '@tldraw/editor'
3
+ import { TLComponents, Tldraw } from '../../lib/Tldraw'
4
+ import { EditLinkDialog } from '../../lib/ui/components/EditLinkDialog'
5
+ import { useDialogs } from '../../lib/ui/context/dialogs'
6
+ import { renderTldrawComponentWithEditor } from '../testutils/renderTldrawComponent'
7
+
8
+ function OpenEditLinkDialogButton() {
9
+ const { addDialog } = useDialogs()
10
+ return (
11
+ <button data-testid="open-edit-link" onClick={() => addDialog({ component: EditLinkDialog })}>
12
+ open
13
+ </button>
14
+ )
15
+ }
16
+
17
+ const components: TLComponents = {
18
+ SharePanel: () => <OpenEditLinkDialogButton />,
19
+ }
20
+
21
+ const shapeId = createShapeId('box')
22
+
23
+ async function renderWithOpenDialog() {
24
+ const { editor } = await renderTldrawComponentWithEditor(
25
+ (onMount) => <Tldraw components={components} onMount={onMount} />,
26
+ { waitForPatterns: false }
27
+ )
28
+
29
+ act(() => {
30
+ editor.createShape({ id: shapeId, type: 'geo', props: { url: 'https://tldraw.com' } })
31
+ editor.select(shapeId)
32
+ })
33
+
34
+ fireEvent.click(screen.getByTestId('open-edit-link'))
35
+ await waitFor(() => expect(getDialogFrame()).not.toBeNull())
36
+
37
+ return editor
38
+ }
39
+
40
+ // The dialog frame that wraps whatever the dialog component renders. The bug left this
41
+ // frame mounted and empty, so checking only for the dialog's own content would not catch it.
42
+ function getDialogFrame() {
43
+ return document.querySelector('.tlui-dialog__content')
44
+ }
45
+
46
+ async function expectDialogClosed() {
47
+ await act(async () => {})
48
+ expect(getDialogFrame()).toBeNull()
49
+ }
50
+
51
+ let editor: Editor
52
+
53
+ afterEach(() => {
54
+ editor?.dispose()
55
+ })
56
+
57
+ it('closes itself when the shape is deleted while the dialog is open', async () => {
58
+ editor = await renderWithOpenDialog()
59
+ expect(getDialogFrame()).not.toBeNull()
60
+
61
+ act(() => {
62
+ editor.deleteShape(shapeId)
63
+ })
64
+
65
+ await expectDialogClosed()
66
+ })
67
+
68
+ it('closes itself when the shape is deselected while the dialog is open', async () => {
69
+ editor = await renderWithOpenDialog()
70
+ expect(getDialogFrame()).not.toBeNull()
71
+
72
+ act(() => {
73
+ editor.selectNone()
74
+ })
75
+
76
+ await expectDialogClosed()
77
+ })
@@ -0,0 +1,58 @@
1
+ import { act, fireEvent, screen } from '@testing-library/react'
2
+ import { createShapeId, Editor, TLImageShape } from '@tldraw/editor'
3
+ import { Tldraw } from '../../lib/Tldraw'
4
+ import { renderTldrawComponentWithEditor } from '../testutils/renderTldrawComponent'
5
+
6
+ let editor: Editor
7
+ const imageId = createShapeId('image') as TLImageShape['id']
8
+
9
+ beforeEach(async () => {
10
+ const result = await renderTldrawComponentWithEditor((onMount) => <Tldraw onMount={onMount} />, {
11
+ waitForPatterns: false,
12
+ })
13
+ editor = result.editor
14
+
15
+ act(() => {
16
+ editor.createShapes([{ id: imageId, type: 'image', x: 0, y: 0, props: { w: 100, h: 100 } }])
17
+ editor.select(imageId)
18
+ })
19
+ })
20
+
21
+ afterEach(() => {
22
+ editor?.dispose()
23
+ })
24
+
25
+ async function enterCropMode() {
26
+ act(() => {
27
+ editor.setCroppingShape(imageId)
28
+ editor.setCurrentTool('select.crop.idle')
29
+ })
30
+ const slider = await screen.findByTestId('tool.image-zoom')
31
+ return slider.querySelector('.tlui-slider__thumb') as HTMLElement
32
+ }
33
+
34
+ describe('Image toolbar in crop mode', () => {
35
+ it('keeps the image selected when Escape is pressed on the zoom slider', async () => {
36
+ const thumb = await enterCropMode()
37
+
38
+ act(() => {
39
+ fireEvent.keyDown(thumb, { key: 'Escape' })
40
+ })
41
+
42
+ expect(editor.getCroppingShapeId()).toBe(null)
43
+ expect(editor.isIn('select.idle')).toBe(true)
44
+ expect(editor.getSelectedShapeIds()).toEqual([imageId])
45
+ })
46
+
47
+ it('keeps the image selected when Enter is pressed on the zoom slider', async () => {
48
+ const thumb = await enterCropMode()
49
+
50
+ act(() => {
51
+ fireEvent.keyDown(thumb, { key: 'Enter' })
52
+ })
53
+
54
+ expect(editor.getCroppingShapeId()).toBe(null)
55
+ expect(editor.isIn('select.idle')).toBe(true)
56
+ expect(editor.getSelectedShapeIds()).toEqual([imageId])
57
+ })
58
+ })
@@ -1,5 +1,5 @@
1
- import { act, screen, waitFor } from '@testing-library/react'
2
- import { DefaultColorStyle, Editor } from '@tldraw/editor'
1
+ import { act, fireEvent, screen, waitFor } from '@testing-library/react'
2
+ import { createShapeId, DefaultColorStyle, Editor, TLArrowShape } from '@tldraw/editor'
3
3
  import { Tldraw } from '../../lib/Tldraw'
4
4
  import { renderTldrawComponentWithEditor } from '../testutils/renderTldrawComponent'
5
5
 
@@ -43,4 +43,35 @@ describe('StylePanel', () => {
43
43
  expect(getBlackColorSwatch().style.color).toBe('rgb(29, 29, 29)')
44
44
  })
45
45
  })
46
+
47
+ it('marks an undo step when changing an arrowhead', async () => {
48
+ const id = createShapeId()
49
+ act(() => {
50
+ editor
51
+ .createShapes<TLArrowShape>([
52
+ {
53
+ id,
54
+ type: 'arrow',
55
+ x: 100,
56
+ y: 100,
57
+ props: { start: { x: 0, y: 0 }, end: { x: 100, y: 100 } },
58
+ },
59
+ ])
60
+ .selectNone()
61
+ editor.markHistoryStoppingPoint('before selecting arrow')
62
+ editor.select(id)
63
+ })
64
+
65
+ fireEvent.click(await screen.findByTestId('style.arrowheadEnd'))
66
+ fireEvent.click(await screen.findByTestId('style.arrowheadEnd.none'))
67
+
68
+ expect(editor.getShape<TLArrowShape>(id)!.props.arrowheadEnd).toBe('none')
69
+
70
+ act(() => {
71
+ editor.undo()
72
+ })
73
+
74
+ expect(editor.getShape<TLArrowShape>(id)!.props.arrowheadEnd).toBe('arrow')
75
+ expect(editor.getSelectedShapeIds()).toEqual([id])
76
+ })
46
77
  })
@@ -314,3 +314,30 @@ describe('shifted number-row shortcuts across keyboard layouts', () => {
314
314
  expect(zoomOut).toHaveBeenCalledTimes(1)
315
315
  })
316
316
  })
317
+
318
+ // Regression test for #10422: frame-selection was the only cmd shortcut without a ctrl twin,
319
+ // so Ctrl+Alt+G did nothing on Windows and Linux even though the shortcuts dialog listed it.
320
+ describe('frame selection shortcut', () => {
321
+ it.each([
322
+ ['cmd+alt+g (macOS)', { metaKey: true }],
323
+ ['ctrl+alt+g (Windows / Linux)', { ctrlKey: true }],
324
+ ])('wraps the selection in a frame on %s', async (_label, modifier) => {
325
+ const { editor } = await setupFocusedEditor()
326
+ const a = createShapeId()
327
+ const b = createShapeId()
328
+ act(() => {
329
+ editor.createShapes([
330
+ { id: a, type: 'geo', x: 0, y: 0 },
331
+ { id: b, type: 'geo', x: 200, y: 200 },
332
+ ])
333
+ editor.select(a, b)
334
+ })
335
+
336
+ keydown(editor, { key: 'g', code: 'KeyG', altKey: true, ...modifier })
337
+
338
+ const frame = editor.getCurrentPageShapes().find((s) => editor.isShapeOfType(s, 'frame'))
339
+ expect(frame).toBeDefined()
340
+ expect(editor.getShape(a)?.parentId).toBe(frame!.id)
341
+ expect(editor.getShape(b)?.parentId).toBe(frame!.id)
342
+ })
343
+ })
@@ -0,0 +1,44 @@
1
+ import { act } from '@testing-library/react'
2
+ import { TLPOINTER_ID } from '@tldraw/editor'
3
+ import { createUserId, InstancePresenceRecordType } from '@tldraw/tlschema'
4
+ import { Tldraw } from '../../lib/Tldraw'
5
+ import { renderTldrawComponentWithEditor } from '../testutils/renderTldrawComponent'
6
+
7
+ it('counts keydown as presence activity even when shortcuts are disabled', async () => {
8
+ const { editor } = await renderTldrawComponentWithEditor(
9
+ (onMount) => <Tldraw onMount={onMount} />,
10
+ { waitForPatterns: false }
11
+ )
12
+
13
+ act(() => {
14
+ // The document key handlers only register while the editor is focused.
15
+ editor.updateInstanceState({ isFocused: true })
16
+ editor.store.put([
17
+ InstancePresenceRecordType.create({
18
+ id: InstancePresenceRecordType.createId('peer'),
19
+ userId: createUserId('peer'),
20
+ userName: 'Peer',
21
+ currentPageId: editor.getCurrentPageId(),
22
+ }),
23
+ ])
24
+ })
25
+
26
+ // Focus a text input inside the container, the same situation as editing a
27
+ // text shape: shortcuts are disabled, so keydown events return before they
28
+ // reach `Editor.dispatch`.
29
+ const input = document.createElement('input')
30
+ editor.getContainer().appendChild(input)
31
+ input.focus()
32
+ expect(document.activeElement).toBe(input)
33
+
34
+ expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(0)
35
+
36
+ act(() => {
37
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', code: 'KeyA', bubbles: true }))
38
+ })
39
+
40
+ // The event never reached the editor as a key_down...
41
+ expect(editor.inputs.keys.has('KeyA')).toBe(false)
42
+ // ...but it still counts as presence activity.
43
+ expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBeGreaterThan(0)
44
+ })
@@ -0,0 +1,55 @@
1
+ import { act } from '@testing-library/react'
2
+ import { createShapeId } from '@tldraw/editor'
3
+ import { useEffect } from 'react'
4
+ import { Tldraw } from '../../lib/Tldraw'
5
+ import { TLUiActionsContextType, useActions } from '../../lib/ui/context/actions'
6
+ import { renderTldrawComponentWithEditor } from '../testutils/renderTldrawComponent'
7
+
8
+ function ActionCapturer({ onCapture }: { onCapture(actions: TLUiActionsContextType): void }) {
9
+ const actions = useActions()
10
+ useEffect(() => {
11
+ onCapture(actions)
12
+ }, [actions, onCapture])
13
+ return null
14
+ }
15
+
16
+ async function setup() {
17
+ let actions: TLUiActionsContextType | null = null
18
+ const { editor } = await renderTldrawComponentWithEditor(
19
+ (onMount) => (
20
+ <Tldraw onMount={onMount}>
21
+ <ActionCapturer onCapture={(a) => (actions = a)} />
22
+ </Tldraw>
23
+ ),
24
+ { waitForPatterns: false }
25
+ )
26
+ return { editor, unlockAll: actions!['unlock-all'] }
27
+ }
28
+
29
+ describe('unlock-all action', () => {
30
+ it('marks a history stopping point so undo does not roll back earlier changes', async () => {
31
+ const { editor, unlockAll } = await setup()
32
+
33
+ const lockedId = createShapeId()
34
+ const laterId = createShapeId()
35
+ act(() => {
36
+ editor.markHistoryStoppingPoint('create locked')
37
+ editor.createShape({ id: lockedId, type: 'geo', x: 0, y: 0, isLocked: true })
38
+ editor.markHistoryStoppingPoint('create later')
39
+ editor.createShape({ id: laterId, type: 'geo', x: 200, y: 0 })
40
+ })
41
+
42
+ act(() => {
43
+ unlockAll.onSelect('context-menu')
44
+ })
45
+ expect(editor.getShape(lockedId)!.isLocked).toBe(false)
46
+
47
+ act(() => {
48
+ editor.undo()
49
+ })
50
+
51
+ // the unlock is undone, but the shape created before it survives
52
+ expect(editor.getShape(lockedId)!.isLocked).toBe(true)
53
+ expect(editor.getShape(laterId)).toBeDefined()
54
+ })
55
+ })
@@ -0,0 +1,74 @@
1
+ import { render } from '@testing-library/react'
2
+ import { ContainerProvider, EditorProvider, useLicenseContext } from '@tldraw/editor'
3
+ import { vi } from 'vitest'
4
+ import { Tldraw } from '../lib/Tldraw'
5
+ import { DefaultToolbar } from '../lib/ui/components/Toolbar/DefaultToolbar'
6
+ import { TldrawUiContextProvider } from '../lib/ui/context/TldrawUiContextProvider'
7
+ import { useCommentingEnabled } from '../lib/ui/hooks/useCommentingEnabled'
8
+ import { renderTldrawComponentWithEditor } from './testutils/renderTldrawComponent'
9
+
10
+ // Default UI components can be mounted beside <Tldraw /> rather than inside it, wrapped in the
11
+ // providers the caller recreates (container, editor, ui context). The license provider is not
12
+ // among them — it only exists inside <TldrawEditor />.
13
+ describe('default UI mounted outside <Tldraw />', () => {
14
+ it('renders the default toolbar and resolves license feature flags through the editor', async () => {
15
+ const { editor } = await renderTldrawComponentWithEditor(
16
+ (onMount) => <Tldraw onMount={onMount} components={{ Toolbar: null }} />,
17
+ { waitForPatterns: true }
18
+ )
19
+
20
+ expect(editor.licenseManager).toBeDefined()
21
+
22
+ let commentingEnabled: boolean | undefined
23
+ function CommentingProbe() {
24
+ commentingEnabled = useCommentingEnabled()
25
+ return null
26
+ }
27
+
28
+ const result = render(
29
+ <ContainerProvider container={editor.getContainer()}>
30
+ <EditorProvider editor={editor}>
31
+ <TldrawUiContextProvider>
32
+ <DefaultToolbar />
33
+ <CommentingProbe />
34
+ </TldrawUiContextProvider>
35
+ </EditorProvider>
36
+ </ContainerProvider>
37
+ )
38
+
39
+ expect(await result.findByTestId('tools.select')).toBeTruthy()
40
+ // In tests the environment counts as development, where every feature is licensed — so a
41
+ // `true` here proves the flag resolved via the editor's license manager, not the
42
+ // fail-closed `false` of having no license manager at all.
43
+ expect(commentingEnabled).toBe(true)
44
+ })
45
+
46
+ it('license feature flags fail closed with no editor and no license context', () => {
47
+ let commentingEnabled: boolean | undefined
48
+ function CommentingProbe() {
49
+ commentingEnabled = useCommentingEnabled()
50
+ return null
51
+ }
52
+
53
+ render(<CommentingProbe />)
54
+
55
+ expect(commentingEnabled).toBe(false)
56
+ })
57
+
58
+ it('useLicenseContext throws when there is no license provider and no editor', () => {
59
+ function LicenseProbe() {
60
+ useLicenseContext()
61
+ return null
62
+ }
63
+
64
+ // silence React's error boundary logging for the expected throw
65
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
66
+ try {
67
+ expect(() => render(<LicenseProbe />)).toThrow(
68
+ 'useLicenseContext must be used inside of the <Tldraw /> or <TldrawEditor /> components'
69
+ )
70
+ } finally {
71
+ consoleError.mockRestore()
72
+ }
73
+ })
74
+ })
@@ -1,6 +1,6 @@
1
+ import { InstancePresenceRecordType, TLUserId, createUserId } from '@tldraw/editor'
1
2
  import { TestEditor } from './TestEditor'
2
3
 
3
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
4
4
  let editor: TestEditor
5
5
 
6
6
  beforeEach(() => {
@@ -8,6 +8,34 @@ beforeEach(() => {
8
8
  })
9
9
 
10
10
  describe('When following a user', () => {
11
+ it('keeps following a leader whose own leader is missing', () => {
12
+ const pageId = editor.getCurrentPageId()
13
+ const leaderId = createUserId('leader')
14
+ const leadersLeaderId = createUserId('leaders-leader')
15
+ const presence = (userId: TLUserId, followingUserId: TLUserId | null) =>
16
+ InstancePresenceRecordType.create({
17
+ id: InstancePresenceRecordType.createId(userId),
18
+ userId,
19
+ userName: userId,
20
+ currentPageId: pageId,
21
+ followingUserId,
22
+ camera: { x: 0, y: 0, z: 1 },
23
+ screenBounds: { x: 0, y: 0, w: 1080, h: 720 },
24
+ lastActivityTimestamp: Date.now(),
25
+ })
26
+
27
+ // the leader follows someone whose presence we don't have
28
+ editor.store.put([presence(leaderId, leadersLeaderId)])
29
+ editor.startFollowingUser(leaderId)
30
+ expect(editor.getInstanceState().followingUserId).toBe(leaderId)
31
+
32
+ // the leader's leader shows up, then leaves again
33
+ editor.store.put([presence(leadersLeaderId, null)])
34
+ expect(editor.getInstanceState().followingUserId).toBe(leaderId)
35
+ editor.store.remove([InstancePresenceRecordType.createId(leadersLeaderId)])
36
+ expect(editor.getInstanceState().followingUserId).toBe(leaderId)
37
+ })
38
+
11
39
  it.todo('starts following a user')
12
40
  it.todo('stops following a user')
13
41
  it.todo('stops following a user when the camera changes due to user action')