tldraw 5.2.0-next.e2b8d10bf10e → 5.2.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 (420) hide show
  1. package/DOCS.md +20294 -0
  2. package/README.md +13 -1
  3. package/RELEASE_NOTES.md +2908 -0
  4. package/dist-cjs/index.d.ts +110 -23
  5. package/dist-cjs/index.js +3 -3
  6. package/dist-cjs/index.js.map +2 -2
  7. package/dist-cjs/lib/TldrawImage.js +0 -1
  8. package/dist-cjs/lib/TldrawImage.js.map +2 -2
  9. package/dist-cjs/lib/bindings/arrow/ArrowBindingUtil.js +2 -1
  10. package/dist-cjs/lib/bindings/arrow/ArrowBindingUtil.js.map +2 -2
  11. package/dist-cjs/lib/defaultEmbedDefinitions.js +3 -2
  12. package/dist-cjs/lib/defaultEmbedDefinitions.js.map +2 -2
  13. package/dist-cjs/lib/defaultOverlayUtils.js +11 -11
  14. package/dist-cjs/lib/defaultOverlayUtils.js.map +2 -2
  15. package/dist-cjs/lib/overlays/SelectionForegroundOverlayUtil.js +76 -64
  16. package/dist-cjs/lib/overlays/SelectionForegroundOverlayUtil.js.map +2 -2
  17. package/dist-cjs/lib/overlays/ShapeHandleOverlayUtil.js +1 -1
  18. package/dist-cjs/lib/overlays/ShapeHandleOverlayUtil.js.map +2 -2
  19. package/dist-cjs/lib/overlays/ShapeIndicatorOverlayUtil.js +107 -0
  20. package/dist-cjs/lib/overlays/ShapeIndicatorOverlayUtil.js.map +7 -0
  21. package/dist-cjs/lib/shapes/arrow/toolStates/Pointing.js +3 -0
  22. package/dist-cjs/lib/shapes/arrow/toolStates/Pointing.js.map +2 -2
  23. package/dist-cjs/lib/shapes/bookmark/BookmarkShapeUtil.js +12 -3
  24. package/dist-cjs/lib/shapes/bookmark/BookmarkShapeUtil.js.map +2 -2
  25. package/dist-cjs/lib/shapes/bookmark/bookmarks.js +54 -6
  26. package/dist-cjs/lib/shapes/bookmark/bookmarks.js.map +2 -2
  27. package/dist-cjs/lib/shapes/draw/DrawShapeUtil.js +2 -1
  28. package/dist-cjs/lib/shapes/draw/DrawShapeUtil.js.map +2 -2
  29. package/dist-cjs/lib/shapes/draw/getPath.js +1 -1
  30. package/dist-cjs/lib/shapes/draw/getPath.js.map +2 -2
  31. package/dist-cjs/lib/shapes/draw/toolStates/Drawing.js +45 -47
  32. package/dist-cjs/lib/shapes/draw/toolStates/Drawing.js.map +2 -2
  33. package/dist-cjs/lib/shapes/embed/EmbedShapeUtil.js +2 -1
  34. package/dist-cjs/lib/shapes/embed/EmbedShapeUtil.js.map +2 -2
  35. package/dist-cjs/lib/shapes/frame/FrameShapeTool.js +23 -18
  36. package/dist-cjs/lib/shapes/frame/FrameShapeTool.js.map +2 -2
  37. package/dist-cjs/lib/shapes/geo/GeoShapeUtil.js +1 -1
  38. package/dist-cjs/lib/shapes/geo/GeoShapeUtil.js.map +2 -2
  39. package/dist-cjs/lib/shapes/geo/toolStates/Pointing.js +3 -0
  40. package/dist-cjs/lib/shapes/geo/toolStates/Pointing.js.map +2 -2
  41. package/dist-cjs/lib/shapes/highlight/HighlightShapeUtil.js +11 -7
  42. package/dist-cjs/lib/shapes/highlight/HighlightShapeUtil.js.map +2 -2
  43. package/dist-cjs/lib/shapes/image/ImageShapeUtil.js +3 -11
  44. package/dist-cjs/lib/shapes/image/ImageShapeUtil.js.map +2 -2
  45. package/dist-cjs/lib/shapes/line/toolStates/Pointing.js +5 -1
  46. package/dist-cjs/lib/shapes/line/toolStates/Pointing.js.map +2 -2
  47. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js +31 -41
  48. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js.map +2 -2
  49. package/dist-cjs/lib/shapes/note/noteHelpers.js +1 -1
  50. package/dist-cjs/lib/shapes/note/noteHelpers.js.map +2 -2
  51. package/dist-cjs/lib/shapes/note/toolStates/Pointing.js +28 -6
  52. package/dist-cjs/lib/shapes/note/toolStates/Pointing.js.map +2 -2
  53. package/dist-cjs/lib/shapes/shared/PlainTextLabel.js +3 -2
  54. package/dist-cjs/lib/shapes/shared/PlainTextLabel.js.map +2 -2
  55. package/dist-cjs/lib/shapes/shared/RichTextLabel.js +3 -3
  56. package/dist-cjs/lib/shapes/shared/RichTextLabel.js.map +2 -2
  57. package/dist-cjs/lib/shapes/shared/defaultStyleDefs.js +17 -3
  58. package/dist-cjs/lib/shapes/shared/defaultStyleDefs.js.map +2 -2
  59. package/dist-cjs/lib/shapes/shared/freehand/core.js +424 -0
  60. package/dist-cjs/lib/shapes/shared/freehand/core.js.map +7 -0
  61. package/dist-cjs/lib/shapes/shared/freehand/fmt.js +106 -0
  62. package/dist-cjs/lib/shapes/shared/freehand/fmt.js.map +7 -0
  63. package/dist-cjs/lib/shapes/shared/freehand/getStroke.js +6 -6
  64. package/dist-cjs/lib/shapes/shared/freehand/getStroke.js.map +2 -2
  65. package/dist-cjs/lib/shapes/shared/freehand/getStrokeOutlinePoints.js +320 -86
  66. package/dist-cjs/lib/shapes/shared/freehand/getStrokeOutlinePoints.js.map +3 -3
  67. package/dist-cjs/lib/shapes/shared/freehand/getStrokePoints.js +20 -96
  68. package/dist-cjs/lib/shapes/shared/freehand/getStrokePoints.js.map +3 -3
  69. package/dist-cjs/lib/shapes/shared/freehand/svg.js +55 -19
  70. package/dist-cjs/lib/shapes/shared/freehand/svg.js.map +2 -2
  71. package/dist-cjs/lib/shapes/shared/freehand/svgInk.js +189 -97
  72. package/dist-cjs/lib/shapes/shared/freehand/svgInk.js.map +3 -3
  73. package/dist-cjs/lib/shapes/shared/freehand/types.js.map +1 -1
  74. package/dist-cjs/lib/shapes/shared/interpolate-props.js +6 -2
  75. package/dist-cjs/lib/shapes/shared/interpolate-props.js.map +2 -2
  76. package/dist-cjs/lib/shapes/text/toolStates/Pointing.js +3 -0
  77. package/dist-cjs/lib/shapes/text/toolStates/Pointing.js.map +2 -2
  78. package/dist-cjs/lib/tools/HandTool/HandTool.js +16 -6
  79. package/dist-cjs/lib/tools/HandTool/HandTool.js.map +2 -2
  80. package/dist-cjs/lib/tools/HandTool/childStates/Dragging.js +10 -1
  81. package/dist-cjs/lib/tools/HandTool/childStates/Dragging.js.map +2 -2
  82. package/dist-cjs/lib/tools/HandTool/childStates/OneFingerZooming.js +84 -0
  83. package/dist-cjs/lib/tools/HandTool/childStates/OneFingerZooming.js.map +7 -0
  84. package/dist-cjs/lib/tools/SelectTool/childStates/Idle.js +12 -0
  85. package/dist-cjs/lib/tools/SelectTool/childStates/Idle.js.map +2 -2
  86. package/dist-cjs/lib/tools/SelectTool/childStates/PointingShape.js +4 -0
  87. package/dist-cjs/lib/tools/SelectTool/childStates/PointingShape.js.map +2 -2
  88. package/dist-cjs/lib/tools/SelectTool/childStates/Resizing.js +107 -46
  89. package/dist-cjs/lib/tools/SelectTool/childStates/Resizing.js.map +2 -2
  90. package/dist-cjs/lib/ui/TldrawUi.js +4 -3
  91. package/dist-cjs/lib/ui/TldrawUi.js.map +2 -2
  92. package/dist-cjs/lib/ui/components/A11y.js +2 -0
  93. package/dist-cjs/lib/ui/components/A11y.js.map +2 -2
  94. package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenu.js +14 -1
  95. package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenu.js.map +2 -2
  96. package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenuContent.js +0 -6
  97. package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenuContent.js.map +2 -2
  98. package/dist-cjs/lib/ui/components/DefaultFollowingIndicator.js.map +2 -2
  99. package/dist-cjs/lib/ui/components/Dialogs.js +17 -37
  100. package/dist-cjs/lib/ui/components/Dialogs.js.map +2 -2
  101. package/dist-cjs/lib/ui/components/HelperButtons/BackToContent.js +19 -4
  102. package/dist-cjs/lib/ui/components/HelperButtons/BackToContent.js.map +2 -2
  103. package/dist-cjs/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.js +12 -2
  104. package/dist-cjs/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.js.map +2 -2
  105. package/dist-cjs/lib/ui/components/Minimap/DefaultMinimap.js +47 -70
  106. package/dist-cjs/lib/ui/components/Minimap/DefaultMinimap.js.map +2 -2
  107. package/dist-cjs/lib/ui/components/Minimap/MinimapManager.js +25 -46
  108. package/dist-cjs/lib/ui/components/Minimap/MinimapManager.js.map +2 -2
  109. package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuAvatar.js.map +2 -2
  110. package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuContent.js.map +2 -2
  111. package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuFacePile.js.map +2 -2
  112. package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuItem.js +1 -1
  113. package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuItem.js.map +2 -2
  114. package/dist-cjs/lib/ui/components/StylePanel/StylePanelButtonPicker.js +1 -0
  115. package/dist-cjs/lib/ui/components/StylePanel/StylePanelButtonPicker.js.map +2 -2
  116. package/dist-cjs/lib/ui/components/menu-items.js +7 -0
  117. package/dist-cjs/lib/ui/components/menu-items.js.map +2 -2
  118. package/dist-cjs/lib/ui/components/primitives/TldrawUiDropdownMenu.js +2 -1
  119. package/dist-cjs/lib/ui/components/primitives/TldrawUiDropdownMenu.js.map +2 -2
  120. package/dist-cjs/lib/ui/components/primitives/TldrawUiIcon.js +14 -3
  121. package/dist-cjs/lib/ui/components/primitives/TldrawUiIcon.js.map +2 -2
  122. package/dist-cjs/lib/ui/components/primitives/TldrawUiInput.js +2 -0
  123. package/dist-cjs/lib/ui/components/primitives/TldrawUiInput.js.map +2 -2
  124. package/dist-cjs/lib/ui/components/primitives/TldrawUiPopover.js +2 -0
  125. package/dist-cjs/lib/ui/components/primitives/TldrawUiPopover.js.map +2 -2
  126. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuCheckboxItem.js +0 -2
  127. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuCheckboxItem.js.map +2 -2
  128. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuSubmenu.js +1 -2
  129. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuSubmenu.js.map +2 -2
  130. package/dist-cjs/lib/ui/context/actions.js +70 -1
  131. package/dist-cjs/lib/ui/context/actions.js.map +2 -2
  132. package/dist-cjs/lib/ui/context/asset-urls.js +5 -8
  133. package/dist-cjs/lib/ui/context/asset-urls.js.map +2 -2
  134. package/dist-cjs/lib/ui/context/events.js.map +2 -2
  135. package/dist-cjs/lib/ui/hooks/useKeyboardShortcuts.js +13 -0
  136. package/dist-cjs/lib/ui/hooks/useKeyboardShortcuts.js.map +2 -2
  137. package/dist-cjs/lib/ui/hooks/useTools.js +1 -1
  138. package/dist-cjs/lib/ui/hooks/useTools.js.map +2 -2
  139. package/dist-cjs/lib/ui/hooks/useTranslation/TLUiTranslationKey.js.map +1 -1
  140. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js +2 -0
  141. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js.map +2 -2
  142. package/dist-cjs/lib/ui/version.js +3 -3
  143. package/dist-cjs/lib/ui/version.js.map +1 -1
  144. package/dist-cjs/lib/utils/embeds/embeds.js +4 -4
  145. package/dist-cjs/lib/utils/embeds/embeds.js.map +2 -2
  146. package/dist-cjs/lib/utils/test-helpers.js +2 -2
  147. package/dist-cjs/lib/utils/test-helpers.js.map +2 -2
  148. package/dist-cjs/lib/utils/text/richText.js +3 -1
  149. package/dist-cjs/lib/utils/text/richText.js.map +2 -2
  150. package/dist-esm/index.d.mts +110 -23
  151. package/dist-esm/index.mjs +5 -3
  152. package/dist-esm/index.mjs.map +2 -2
  153. package/dist-esm/lib/TldrawImage.mjs +0 -1
  154. package/dist-esm/lib/TldrawImage.mjs.map +2 -2
  155. package/dist-esm/lib/bindings/arrow/ArrowBindingUtil.mjs +2 -1
  156. package/dist-esm/lib/bindings/arrow/ArrowBindingUtil.mjs.map +2 -2
  157. package/dist-esm/lib/defaultEmbedDefinitions.mjs +3 -2
  158. package/dist-esm/lib/defaultEmbedDefinitions.mjs.map +2 -2
  159. package/dist-esm/lib/defaultOverlayUtils.mjs +11 -11
  160. package/dist-esm/lib/defaultOverlayUtils.mjs.map +2 -2
  161. package/dist-esm/lib/overlays/SelectionForegroundOverlayUtil.mjs +77 -65
  162. package/dist-esm/lib/overlays/SelectionForegroundOverlayUtil.mjs.map +2 -2
  163. package/dist-esm/lib/overlays/ShapeHandleOverlayUtil.mjs +1 -1
  164. package/dist-esm/lib/overlays/ShapeHandleOverlayUtil.mjs.map +2 -2
  165. package/dist-esm/lib/overlays/ShapeIndicatorOverlayUtil.mjs +87 -0
  166. package/dist-esm/lib/overlays/ShapeIndicatorOverlayUtil.mjs.map +7 -0
  167. package/dist-esm/lib/shapes/arrow/toolStates/Pointing.mjs +9 -1
  168. package/dist-esm/lib/shapes/arrow/toolStates/Pointing.mjs.map +2 -2
  169. package/dist-esm/lib/shapes/bookmark/BookmarkShapeUtil.mjs +15 -3
  170. package/dist-esm/lib/shapes/bookmark/BookmarkShapeUtil.mjs.map +2 -2
  171. package/dist-esm/lib/shapes/bookmark/bookmarks.mjs +54 -6
  172. package/dist-esm/lib/shapes/bookmark/bookmarks.mjs.map +2 -2
  173. package/dist-esm/lib/shapes/draw/DrawShapeUtil.mjs +3 -1
  174. package/dist-esm/lib/shapes/draw/DrawShapeUtil.mjs.map +2 -2
  175. package/dist-esm/lib/shapes/draw/getPath.mjs +1 -1
  176. package/dist-esm/lib/shapes/draw/getPath.mjs.map +2 -2
  177. package/dist-esm/lib/shapes/draw/toolStates/Drawing.mjs +46 -47
  178. package/dist-esm/lib/shapes/draw/toolStates/Drawing.mjs.map +2 -2
  179. package/dist-esm/lib/shapes/embed/EmbedShapeUtil.mjs +2 -1
  180. package/dist-esm/lib/shapes/embed/EmbedShapeUtil.mjs.map +2 -2
  181. package/dist-esm/lib/shapes/frame/FrameShapeTool.mjs +23 -18
  182. package/dist-esm/lib/shapes/frame/FrameShapeTool.mjs.map +2 -2
  183. package/dist-esm/lib/shapes/geo/GeoShapeUtil.mjs +2 -1
  184. package/dist-esm/lib/shapes/geo/GeoShapeUtil.mjs.map +2 -2
  185. package/dist-esm/lib/shapes/geo/toolStates/Pointing.mjs +4 -0
  186. package/dist-esm/lib/shapes/geo/toolStates/Pointing.mjs.map +2 -2
  187. package/dist-esm/lib/shapes/highlight/HighlightShapeUtil.mjs +12 -7
  188. package/dist-esm/lib/shapes/highlight/HighlightShapeUtil.mjs.map +2 -2
  189. package/dist-esm/lib/shapes/image/ImageShapeUtil.mjs +3 -11
  190. package/dist-esm/lib/shapes/image/ImageShapeUtil.mjs.map +2 -2
  191. package/dist-esm/lib/shapes/line/toolStates/Pointing.mjs +6 -1
  192. package/dist-esm/lib/shapes/line/toolStates/Pointing.mjs.map +2 -2
  193. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs +32 -42
  194. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs.map +2 -2
  195. package/dist-esm/lib/shapes/note/noteHelpers.mjs +1 -1
  196. package/dist-esm/lib/shapes/note/noteHelpers.mjs.map +2 -2
  197. package/dist-esm/lib/shapes/note/toolStates/Pointing.mjs +29 -6
  198. package/dist-esm/lib/shapes/note/toolStates/Pointing.mjs.map +2 -2
  199. package/dist-esm/lib/shapes/shared/PlainTextLabel.mjs +3 -2
  200. package/dist-esm/lib/shapes/shared/PlainTextLabel.mjs.map +2 -2
  201. package/dist-esm/lib/shapes/shared/RichTextLabel.mjs +4 -3
  202. package/dist-esm/lib/shapes/shared/RichTextLabel.mjs.map +2 -2
  203. package/dist-esm/lib/shapes/shared/defaultStyleDefs.mjs +17 -3
  204. package/dist-esm/lib/shapes/shared/defaultStyleDefs.mjs.map +2 -2
  205. package/dist-esm/lib/shapes/shared/freehand/core.mjs +404 -0
  206. package/dist-esm/lib/shapes/shared/freehand/core.mjs.map +7 -0
  207. package/dist-esm/lib/shapes/shared/freehand/fmt.mjs +86 -0
  208. package/dist-esm/lib/shapes/shared/freehand/fmt.mjs.map +7 -0
  209. package/dist-esm/lib/shapes/shared/freehand/getStroke.mjs +7 -7
  210. package/dist-esm/lib/shapes/shared/freehand/getStroke.mjs.map +2 -2
  211. package/dist-esm/lib/shapes/shared/freehand/getStrokeOutlinePoints.mjs +332 -86
  212. package/dist-esm/lib/shapes/shared/freehand/getStrokeOutlinePoints.mjs.map +3 -3
  213. package/dist-esm/lib/shapes/shared/freehand/getStrokePoints.mjs +31 -96
  214. package/dist-esm/lib/shapes/shared/freehand/getStrokePoints.mjs.map +3 -3
  215. package/dist-esm/lib/shapes/shared/freehand/svg.mjs +55 -19
  216. package/dist-esm/lib/shapes/shared/freehand/svg.mjs.map +2 -2
  217. package/dist-esm/lib/shapes/shared/freehand/svgInk.mjs +214 -99
  218. package/dist-esm/lib/shapes/shared/freehand/svgInk.mjs.map +3 -3
  219. package/dist-esm/lib/shapes/shared/interpolate-props.mjs +6 -2
  220. package/dist-esm/lib/shapes/shared/interpolate-props.mjs.map +2 -2
  221. package/dist-esm/lib/shapes/text/toolStates/Pointing.mjs +4 -0
  222. package/dist-esm/lib/shapes/text/toolStates/Pointing.mjs.map +2 -2
  223. package/dist-esm/lib/tools/HandTool/HandTool.mjs +16 -6
  224. package/dist-esm/lib/tools/HandTool/HandTool.mjs.map +2 -2
  225. package/dist-esm/lib/tools/HandTool/childStates/Dragging.mjs +10 -1
  226. package/dist-esm/lib/tools/HandTool/childStates/Dragging.mjs.map +2 -2
  227. package/dist-esm/lib/tools/HandTool/childStates/OneFingerZooming.mjs +64 -0
  228. package/dist-esm/lib/tools/HandTool/childStates/OneFingerZooming.mjs.map +7 -0
  229. package/dist-esm/lib/tools/SelectTool/childStates/Idle.mjs +12 -0
  230. package/dist-esm/lib/tools/SelectTool/childStates/Idle.mjs.map +2 -2
  231. package/dist-esm/lib/tools/SelectTool/childStates/PointingShape.mjs +4 -0
  232. package/dist-esm/lib/tools/SelectTool/childStates/PointingShape.mjs.map +2 -2
  233. package/dist-esm/lib/tools/SelectTool/childStates/Resizing.mjs +108 -46
  234. package/dist-esm/lib/tools/SelectTool/childStates/Resizing.mjs.map +2 -2
  235. package/dist-esm/lib/ui/TldrawUi.mjs +4 -3
  236. package/dist-esm/lib/ui/TldrawUi.mjs.map +2 -2
  237. package/dist-esm/lib/ui/components/A11y.mjs +2 -0
  238. package/dist-esm/lib/ui/components/A11y.mjs.map +2 -2
  239. package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenu.mjs +21 -2
  240. package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenu.mjs.map +2 -2
  241. package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenuContent.mjs +0 -6
  242. package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenuContent.mjs.map +2 -2
  243. package/dist-esm/lib/ui/components/DefaultFollowingIndicator.mjs.map +2 -2
  244. package/dist-esm/lib/ui/components/Dialogs.mjs +19 -39
  245. package/dist-esm/lib/ui/components/Dialogs.mjs.map +2 -2
  246. package/dist-esm/lib/ui/components/HelperButtons/BackToContent.mjs +20 -5
  247. package/dist-esm/lib/ui/components/HelperButtons/BackToContent.mjs.map +2 -2
  248. package/dist-esm/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.mjs +12 -2
  249. package/dist-esm/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.mjs.map +2 -2
  250. package/dist-esm/lib/ui/components/Minimap/DefaultMinimap.mjs +47 -70
  251. package/dist-esm/lib/ui/components/Minimap/DefaultMinimap.mjs.map +2 -2
  252. package/dist-esm/lib/ui/components/Minimap/MinimapManager.mjs +25 -46
  253. package/dist-esm/lib/ui/components/Minimap/MinimapManager.mjs.map +2 -2
  254. package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuAvatar.mjs.map +2 -2
  255. package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuContent.mjs.map +2 -2
  256. package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuFacePile.mjs.map +2 -2
  257. package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuItem.mjs +1 -1
  258. package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuItem.mjs.map +2 -2
  259. package/dist-esm/lib/ui/components/StylePanel/StylePanelButtonPicker.mjs +1 -0
  260. package/dist-esm/lib/ui/components/StylePanel/StylePanelButtonPicker.mjs.map +2 -2
  261. package/dist-esm/lib/ui/components/menu-items.mjs +7 -0
  262. package/dist-esm/lib/ui/components/menu-items.mjs.map +2 -2
  263. package/dist-esm/lib/ui/components/primitives/TldrawUiDropdownMenu.mjs +2 -1
  264. package/dist-esm/lib/ui/components/primitives/TldrawUiDropdownMenu.mjs.map +2 -2
  265. package/dist-esm/lib/ui/components/primitives/TldrawUiIcon.mjs +14 -3
  266. package/dist-esm/lib/ui/components/primitives/TldrawUiIcon.mjs.map +2 -2
  267. package/dist-esm/lib/ui/components/primitives/TldrawUiInput.mjs +2 -0
  268. package/dist-esm/lib/ui/components/primitives/TldrawUiInput.mjs.map +2 -2
  269. package/dist-esm/lib/ui/components/primitives/TldrawUiPopover.mjs +2 -0
  270. package/dist-esm/lib/ui/components/primitives/TldrawUiPopover.mjs.map +2 -2
  271. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuCheckboxItem.mjs +0 -2
  272. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuCheckboxItem.mjs.map +2 -2
  273. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuSubmenu.mjs +1 -2
  274. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuSubmenu.mjs.map +2 -2
  275. package/dist-esm/lib/ui/context/actions.mjs +71 -1
  276. package/dist-esm/lib/ui/context/actions.mjs.map +2 -2
  277. package/dist-esm/lib/ui/context/asset-urls.mjs +5 -8
  278. package/dist-esm/lib/ui/context/asset-urls.mjs.map +2 -2
  279. package/dist-esm/lib/ui/context/events.mjs.map +2 -2
  280. package/dist-esm/lib/ui/hooks/useKeyboardShortcuts.mjs +13 -0
  281. package/dist-esm/lib/ui/hooks/useKeyboardShortcuts.mjs.map +2 -2
  282. package/dist-esm/lib/ui/hooks/useTools.mjs +1 -1
  283. package/dist-esm/lib/ui/hooks/useTools.mjs.map +2 -2
  284. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs +2 -0
  285. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs.map +2 -2
  286. package/dist-esm/lib/ui/version.mjs +3 -3
  287. package/dist-esm/lib/ui/version.mjs.map +1 -1
  288. package/dist-esm/lib/utils/embeds/embeds.mjs +4 -4
  289. package/dist-esm/lib/utils/embeds/embeds.mjs.map +2 -2
  290. package/dist-esm/lib/utils/test-helpers.mjs +2 -2
  291. package/dist-esm/lib/utils/test-helpers.mjs.map +2 -2
  292. package/dist-esm/lib/utils/text/richText.mjs +3 -1
  293. package/dist-esm/lib/utils/text/richText.mjs.map +2 -2
  294. package/package.json +11 -6
  295. package/src/index.ts +6 -1
  296. package/src/lib/TldrawImage.tsx +2 -2
  297. package/src/lib/bindings/arrow/ArrowBindingUtil.ts +5 -1
  298. package/src/lib/defaultEmbedDefinitions.ts +28 -6
  299. package/src/lib/defaultOverlayUtils.ts +11 -11
  300. package/src/lib/overlays/SelectionForegroundOverlayUtil.ts +91 -72
  301. package/src/lib/overlays/ShapeHandleOverlayUtil.ts +3 -1
  302. package/src/lib/overlays/ShapeIndicatorOverlayUtil.ts +133 -0
  303. package/src/lib/shapes/arrow/toolStates/Pointing.tsx +11 -1
  304. package/src/lib/shapes/bookmark/BookmarkShapeUtil.tsx +16 -3
  305. package/src/lib/shapes/bookmark/bookmarks.ts +105 -15
  306. package/src/lib/shapes/draw/DrawShapeUtil.tsx +7 -5
  307. package/src/lib/shapes/draw/getPath.ts +1 -1
  308. package/src/lib/shapes/draw/toolStates/Drawing.ts +55 -48
  309. package/src/lib/shapes/embed/EmbedShapeUtil.tsx +14 -4
  310. package/src/lib/shapes/frame/FrameShapeTool.ts +35 -24
  311. package/src/lib/shapes/geo/GeoShapeUtil.test.tsx +46 -0
  312. package/src/lib/shapes/geo/GeoShapeUtil.tsx +2 -1
  313. package/src/lib/shapes/geo/toolStates/Pointing.ts +5 -0
  314. package/src/lib/shapes/highlight/HighlightShapeUtil.tsx +14 -11
  315. package/src/lib/shapes/image/ImageShapeUtil.tsx +7 -11
  316. package/src/lib/shapes/line/LineShapeTool.test.ts +41 -0
  317. package/src/lib/shapes/line/toolStates/Pointing.ts +12 -2
  318. package/src/lib/shapes/note/NoteShapeUtil.tsx +36 -51
  319. package/src/lib/shapes/note/noteCloning.test.ts +5 -5
  320. package/src/lib/shapes/note/noteHelpers.ts +1 -1
  321. package/src/lib/shapes/note/toolStates/Pointing.ts +40 -7
  322. package/src/lib/shapes/shared/PlainTextLabel.tsx +3 -3
  323. package/src/lib/shapes/shared/RichTextLabel.tsx +4 -3
  324. package/src/lib/shapes/shared/defaultStyleDefs.tsx +25 -3
  325. package/src/lib/shapes/shared/freehand/core.ts +547 -0
  326. package/src/lib/shapes/shared/freehand/fmt.ts +106 -0
  327. package/src/lib/shapes/shared/freehand/getStroke.ts +7 -7
  328. package/src/lib/shapes/shared/freehand/getStrokeOutlinePoints.ts +451 -139
  329. package/src/lib/shapes/shared/freehand/getStrokePoints.ts +33 -153
  330. package/src/lib/shapes/shared/freehand/svg.ts +72 -23
  331. package/src/lib/shapes/shared/freehand/svgInk.ts +281 -136
  332. package/src/lib/shapes/shared/freehand/types.ts +0 -1
  333. package/src/lib/shapes/shared/interpolate-props.ts +6 -2
  334. package/src/lib/shapes/text/toolStates/Pointing.ts +5 -0
  335. package/src/lib/tools/HandTool/HandTool.ts +19 -6
  336. package/src/lib/tools/HandTool/childStates/Dragging.ts +17 -1
  337. package/src/lib/tools/HandTool/childStates/OneFingerZooming.ts +79 -0
  338. package/src/lib/tools/SelectTool/childStates/Idle.ts +16 -2
  339. package/src/lib/tools/SelectTool/childStates/PointingShape.ts +8 -0
  340. package/src/lib/tools/SelectTool/childStates/Resizing.ts +137 -49
  341. package/src/lib/ui/TldrawUi.tsx +3 -3
  342. package/src/lib/ui/components/A11y.tsx +2 -0
  343. package/src/lib/ui/components/ContextMenu/DefaultContextMenu.tsx +32 -2
  344. package/src/lib/ui/components/ContextMenu/DefaultContextMenuContent.tsx +5 -6
  345. package/src/lib/ui/components/DefaultFollowingIndicator.tsx +2 -2
  346. package/src/lib/ui/components/Dialogs.tsx +20 -23
  347. package/src/lib/ui/components/HelperButtons/BackToContent.tsx +32 -4
  348. package/src/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent.tsx +10 -0
  349. package/src/lib/ui/components/Minimap/DefaultMinimap.tsx +60 -74
  350. package/src/lib/ui/components/Minimap/MinimapManager.ts +35 -57
  351. package/src/lib/ui/components/SharePanel/DefaultPeopleMenuAvatar.tsx +2 -2
  352. package/src/lib/ui/components/SharePanel/DefaultPeopleMenuContent.tsx +2 -1
  353. package/src/lib/ui/components/SharePanel/DefaultPeopleMenuFacePile.tsx +2 -1
  354. package/src/lib/ui/components/SharePanel/DefaultPeopleMenuItem.tsx +3 -3
  355. package/src/lib/ui/components/StylePanel/StylePanelButtonPicker.tsx +5 -0
  356. package/src/lib/ui/components/menu-items.tsx +9 -0
  357. package/src/lib/ui/components/primitives/TldrawUiDropdownMenu.tsx +3 -1
  358. package/src/lib/ui/components/primitives/TldrawUiIcon.tsx +15 -3
  359. package/src/lib/ui/components/primitives/TldrawUiInput.tsx +4 -0
  360. package/src/lib/ui/components/primitives/TldrawUiPopover.tsx +7 -0
  361. package/src/lib/ui/components/primitives/menus/TldrawUiMenuCheckboxItem.tsx +0 -2
  362. package/src/lib/ui/components/primitives/menus/TldrawUiMenuSubmenu.tsx +0 -1
  363. package/src/lib/ui/context/actions.tsx +88 -1
  364. package/src/lib/ui/context/asset-urls.tsx +9 -8
  365. package/src/lib/ui/context/events.tsx +2 -0
  366. package/src/lib/ui/hooks/useKeyboardShortcuts.ts +45 -7
  367. package/src/lib/ui/hooks/useTools.tsx +7 -1
  368. package/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts +2 -0
  369. package/src/lib/ui/hooks/useTranslation/defaultTranslation.ts +2 -0
  370. package/src/lib/ui/version.ts +3 -3
  371. package/src/lib/ui.css +20 -9
  372. package/src/lib/utils/embeds/embeds.test.ts +14 -12
  373. package/src/lib/utils/embeds/embeds.ts +10 -4
  374. package/src/lib/utils/test-helpers.ts +3 -3
  375. package/src/lib/utils/text/richText.test.ts +36 -0
  376. package/src/lib/utils/text/richText.ts +9 -1
  377. package/src/test/HandTool.test.ts +93 -0
  378. package/src/test/ImageShapeUtil.test.ts +88 -0
  379. package/src/test/SelectTool.test.ts +102 -1
  380. package/src/test/__snapshots__/drawing.test.ts.snap +2 -1
  381. package/src/test/attribution.test.ts +40 -29
  382. package/src/test/bookmark-shapes.test.ts +256 -28
  383. package/src/test/commands/__snapshots__/getSvgString.test.ts.snap +2 -2
  384. package/src/test/commands/clipboardCallbacks.test.ts +3 -1
  385. package/src/test/commands/getSvgString.test.ts +30 -0
  386. package/src/test/commands/penmode.test.ts +97 -1
  387. package/src/test/commands/setCamera.test.ts +12 -0
  388. package/src/test/dragFromToolbar.test.ts +67 -0
  389. package/src/test/drawing.test.ts +48 -7
  390. package/src/test/frames.test.ts +72 -0
  391. package/src/test/freehand/corpus/presets.ts +71 -0
  392. package/src/test/freehand/corpus/real.ts +370 -0
  393. package/src/test/freehand/corpus/strokes.ts +50 -0
  394. package/src/test/freehand/svg.test.ts +20 -0
  395. package/src/test/freehand/wellformed.test.ts +30 -0
  396. package/src/test/long-press-create.test.ts +102 -0
  397. package/src/test/overlays/CollaboratorBrushOverlayUtil.test.ts +4 -4
  398. package/src/test/overlays/CollaboratorCursorOverlayUtil.test.ts +18 -18
  399. package/src/test/overlays/CollaboratorHintOverlayUtil.test.ts +7 -7
  400. package/src/test/overlays/CollaboratorScribbleOverlayUtil.test.ts +3 -3
  401. package/src/test/overlays/OverlayManager.test.ts +1 -1
  402. package/src/test/overlays/SelectionForegroundOverlayUtil.test.ts +58 -0
  403. package/src/test/overlays/ShapeHandleOverlayUtil.test.ts +2 -2
  404. package/src/test/overlays/ShapeIndicatorOverlayUtil.test.ts +36 -0
  405. package/src/test/resizing.test.ts +250 -0
  406. package/src/test/right-click-context-menu.test.ts +90 -0
  407. package/src/test/rightClickPanning.test.ts +11 -0
  408. package/src/test/selection-omnibus.test.ts +15 -2
  409. package/src/test/spatialIndex.test.ts +132 -0
  410. package/src/test/translating.test.ts +0 -6
  411. package/src/test/ui/ContextMenu.test.tsx +146 -3
  412. package/src/test/ui/Dialogs.test.tsx +256 -0
  413. package/src/test/ui/copyHoveredStyles.test.tsx +154 -0
  414. package/src/test/ui/keyboardShortcuts.test.tsx +266 -0
  415. package/tldraw.css +35 -12
  416. package/dist-cjs/lib/shapes/shared/freehand/setStrokePointRadii.js +0 -100
  417. package/dist-cjs/lib/shapes/shared/freehand/setStrokePointRadii.js.map +0 -7
  418. package/dist-esm/lib/shapes/shared/freehand/setStrokePointRadii.mjs +0 -80
  419. package/dist-esm/lib/shapes/shared/freehand/setStrokePointRadii.mjs.map +0 -7
  420. package/src/lib/shapes/shared/freehand/setStrokePointRadii.ts +0 -127
@@ -1,8 +1,11 @@
1
- import { fireEvent, screen } from '@testing-library/react'
2
- import { createShapeId } from '@tldraw/editor'
1
+ import { act, fireEvent, screen } from '@testing-library/react'
2
+ import { createShapeId, tlenvReactive } from '@tldraw/editor'
3
3
  import { TLComponents, Tldraw } from '../../lib/Tldraw'
4
4
  import { DefaultContextMenu } from '../../lib/ui/components/ContextMenu/DefaultContextMenu'
5
- import { renderTldrawComponent } from '../testutils/renderTldrawComponent'
5
+ import {
6
+ renderTldrawComponent,
7
+ renderTldrawComponentWithEditor,
8
+ } from '../testutils/renderTldrawComponent'
6
9
 
7
10
  it('opens on right-click', async () => {
8
11
  await renderTldrawComponent(
@@ -23,6 +26,146 @@ it('opens on right-click', async () => {
23
26
  expect(screen.queryByTestId('context-menu')).toBeNull()
24
27
  })
25
28
 
29
+ // A touch long-press (coarse pointer) opens the menu only in the select tool. In any
30
+ // other tool the long-press belongs to that tool's gesture, so the menu stays closed.
31
+ // The instance's isCoarsePointer is synced from tlenv, so flip that to simulate touch.
32
+ describe('on a coarse pointer (touch long-press)', () => {
33
+ beforeEach(() => {
34
+ tlenvReactive.update((prev) => ({ ...prev, isCoarsePointer: true }))
35
+ })
36
+ afterEach(() => {
37
+ tlenvReactive.update((prev) => ({ ...prev, isCoarsePointer: false }))
38
+ })
39
+
40
+ it('opens the menu in the select tool', async () => {
41
+ await renderTldrawComponent(
42
+ <Tldraw
43
+ onMount={(editor) => {
44
+ editor.createShape({ id: createShapeId(), type: 'geo' })
45
+ }}
46
+ />,
47
+ { waitForPatterns: false }
48
+ )
49
+ const canvas = await screen.findByTestId('canvas')
50
+
51
+ fireEvent.contextMenu(canvas)
52
+ await screen.findByTestId('context-menu')
53
+ })
54
+
55
+ it.each([
56
+ 'geo',
57
+ 'note',
58
+ 'line',
59
+ 'text',
60
+ 'arrow',
61
+ 'frame',
62
+ 'eraser',
63
+ 'draw',
64
+ 'highlight',
65
+ 'laser',
66
+ 'hand',
67
+ 'zoom',
68
+ ])('does not open the menu in the %s tool', async (tool) => {
69
+ await renderTldrawComponent(
70
+ <Tldraw
71
+ onMount={(editor) => {
72
+ editor.createShape({ id: createShapeId(), type: 'geo' })
73
+ editor.setCurrentTool(tool)
74
+ }}
75
+ />,
76
+ { waitForPatterns: false }
77
+ )
78
+ const canvas = await screen.findByTestId('canvas')
79
+
80
+ fireEvent.contextMenu(canvas)
81
+ expect(screen.queryByTestId('context-menu')).toBeNull()
82
+ })
83
+ })
84
+
85
+ // A right-click (fine pointer) opens the menu in any tool — the editor routes the
86
+ // click through the select tool. Regression guard: gating on the tool instead of the
87
+ // pointer type used to suppress this because the switch-to-select lagged the render.
88
+ it.each(['select', 'geo', 'note', 'eraser', 'draw'])(
89
+ 'opens on right-click in the %s tool',
90
+ async (tool) => {
91
+ await renderTldrawComponent(
92
+ <Tldraw
93
+ onMount={(editor) => {
94
+ editor.createShape({ id: createShapeId(), type: 'geo' })
95
+ editor.setCurrentTool(tool)
96
+ }}
97
+ />,
98
+ { waitForPatterns: false }
99
+ )
100
+ const canvas = await screen.findByTestId('canvas')
101
+
102
+ fireEvent.contextMenu(canvas)
103
+ await screen.findByTestId('context-menu')
104
+ }
105
+ )
106
+
107
+ it('right-click in a shape tool reveals shape actions (rightClickPanning off)', async () => {
108
+ // Regression for #8277: a right-click while a shape-creation tool is active
109
+ // should switch to the select tool and select the shape under the pointer
110
+ // *before* the menu opens, so the menu carries the full set of shape actions.
111
+ // With rightClickPanning off the native contextmenu fires on press, so the
112
+ // switch must complete synchronously on the pointer-down — this asserts the
113
+ // switch beats the menu.
114
+ const id = createShapeId()
115
+ const { editor } = await renderTldrawComponentWithEditor(
116
+ (onMount) => (
117
+ <Tldraw
118
+ options={{ rightClickPanning: false }}
119
+ onMount={(editor) => {
120
+ editor.createShape({
121
+ id,
122
+ type: 'geo',
123
+ x: 0,
124
+ y: 0,
125
+ props: { w: 100, h: 100, geo: 'rectangle' },
126
+ })
127
+ editor.setCurrentTool('geo')
128
+ onMount(editor)
129
+ }}
130
+ />
131
+ ),
132
+ { waitForPatterns: false }
133
+ )
134
+
135
+ const canvas = await screen.findByTestId('canvas')
136
+
137
+ // right-click over the shape: the button-2 pointer-down dispatches a
138
+ // `right_click`, which the geo tool's idle routes through the select tool.
139
+ act(() => {
140
+ editor.dispatch({
141
+ type: 'pointer',
142
+ name: 'pointer_down',
143
+ target: 'canvas',
144
+ point: { x: 50, y: 50, z: 0.5 },
145
+ pointerId: 1,
146
+ button: 2,
147
+ isPen: false,
148
+ shiftKey: false,
149
+ altKey: false,
150
+ ctrlKey: false,
151
+ metaKey: false,
152
+ accelKey: false,
153
+ })
154
+ })
155
+
156
+ // the switch beats the menu: select is active and the shape is selected
157
+ // before the contextmenu event is ever handled
158
+ expect(editor.getCurrentToolId()).toBe('select')
159
+ expect(editor.getSelectedShapeIds()).toEqual([id])
160
+
161
+ fireEvent.contextMenu(canvas)
162
+ await screen.findByTestId('context-menu')
163
+ // shape actions are present because the shape is selected
164
+ await screen.findByTestId('context-menu.cut')
165
+ await screen.findByTestId('context-menu.copy')
166
+ await screen.findByTestId('context-menu.delete')
167
+ })
168
+
26
169
  it('tunnels context menu', async () => {
27
170
  const components: TLComponents = {
28
171
  ContextMenu: (props) => {
@@ -0,0 +1,256 @@
1
+ import { fireEvent, screen } from '@testing-library/react'
2
+ import { Select as _Select } from 'radix-ui'
3
+ import { TLComponents, Tldraw } from '../../lib/Tldraw'
4
+ import { useDialogs } from '../../lib/ui/context/dialogs'
5
+ import { renderTldrawComponent } from '../testutils/renderTldrawComponent'
6
+
7
+ // jsdom doesn't implement scrollIntoView, which Radix Select calls when it opens.
8
+ Element.prototype.scrollIntoView ??= () => {}
9
+
10
+ function DialogWithSelect({ onClose }: { onClose(): void }) {
11
+ return (
12
+ <div data-testid="dialog-inner">
13
+ <_Select.Root defaultValue="a">
14
+ <_Select.Trigger data-testid="select-trigger">
15
+ <_Select.Value />
16
+ </_Select.Trigger>
17
+ <_Select.Portal>
18
+ <_Select.Content data-testid="select-content">
19
+ <_Select.Viewport>
20
+ <_Select.Item value="a" data-testid="select-a">
21
+ <_Select.ItemText>A</_Select.ItemText>
22
+ </_Select.Item>
23
+ <_Select.Item value="b" data-testid="select-b">
24
+ <_Select.ItemText>B</_Select.ItemText>
25
+ </_Select.Item>
26
+ </_Select.Viewport>
27
+ </_Select.Content>
28
+ </_Select.Portal>
29
+ </_Select.Root>
30
+ <button data-testid="dialog-close" onClick={onClose}>
31
+ Close
32
+ </button>
33
+ </div>
34
+ )
35
+ }
36
+
37
+ function OpenDialogButton({ preventBackgroundClose }: { preventBackgroundClose?: boolean }) {
38
+ const { addDialog } = useDialogs()
39
+ return (
40
+ <button
41
+ data-testid="open-dialog"
42
+ onClick={() => addDialog({ component: DialogWithSelect, preventBackgroundClose })}
43
+ >
44
+ open
45
+ </button>
46
+ )
47
+ }
48
+
49
+ // A confirm-style dialog opened from inside another dialog (e.g. the dotcom workspace
50
+ // settings dialog's regenerate/leave/delete confirmations).
51
+ function NestedConfirmDialog({ onClose }: { onClose(): void }) {
52
+ return (
53
+ <div data-testid="nested-dialog">
54
+ <button data-testid="nested-confirm" onClick={onClose}>
55
+ Confirm
56
+ </button>
57
+ </div>
58
+ )
59
+ }
60
+
61
+ function DialogThatOpensNested({ onClose }: { onClose(): void }) {
62
+ const { addDialog } = useDialogs()
63
+ return (
64
+ <div data-testid="parent-dialog">
65
+ <button
66
+ data-testid="open-nested"
67
+ onClick={() => addDialog({ component: NestedConfirmDialog })}
68
+ >
69
+ open nested
70
+ </button>
71
+ <button data-testid="parent-close" onClick={onClose}>
72
+ Close
73
+ </button>
74
+ </div>
75
+ )
76
+ }
77
+
78
+ function OpenParentDialogButton() {
79
+ const { addDialog } = useDialogs()
80
+ return (
81
+ <button
82
+ data-testid="open-parent"
83
+ onClick={() => addDialog({ component: DialogThatOpensNested })}
84
+ >
85
+ open parent
86
+ </button>
87
+ )
88
+ }
89
+
90
+ function renderWithParentDialog() {
91
+ const components: TLComponents = {
92
+ SharePanel: () => <OpenParentDialogButton />,
93
+ }
94
+ return renderTldrawComponent(<Tldraw components={components} />, { waitForPatterns: false })
95
+ }
96
+
97
+ function renderWithDialog(opts: { preventBackgroundClose?: boolean } = {}) {
98
+ const components: TLComponents = {
99
+ SharePanel: () => <OpenDialogButton preventBackgroundClose={opts.preventBackgroundClose} />,
100
+ }
101
+ return renderTldrawComponent(<Tldraw components={components} />, { waitForPatterns: false })
102
+ }
103
+
104
+ // A full press on the dialog backdrop. Radix dismisses on pointer-down, but we fire the
105
+ // whole sequence so a layer-blind click handler (the bug this guards against) would be caught.
106
+ function pressBackdrop() {
107
+ const el = document.querySelector('.tlui-dialog__positioner')
108
+ if (!el) throw new Error('dialog positioner (backdrop) not found')
109
+ fireEvent.pointerDown(el, { pointerType: 'mouse' })
110
+ fireEvent.pointerUp(el, { pointerType: 'mouse' })
111
+ fireEvent.click(el)
112
+ }
113
+
114
+ // Press the backdrop of the topmost dialog (the last positioner in the DOM) — used when
115
+ // dialogs are stacked, so the press lands on the layer that should dismiss first.
116
+ function pressTopBackdrop() {
117
+ const positioners = document.querySelectorAll('.tlui-dialog__positioner')
118
+ const el = positioners[positioners.length - 1]
119
+ if (!el) throw new Error('dialog positioner (backdrop) not found')
120
+ fireEvent.pointerDown(el, { pointerType: 'mouse' })
121
+ fireEvent.pointerUp(el, { pointerType: 'mouse' })
122
+ fireEvent.click(el)
123
+ }
124
+
125
+ it('dismisses a select opened inside a modal before the modal itself', async () => {
126
+ await renderWithDialog()
127
+
128
+ fireEvent.click(screen.getByTestId('open-dialog'))
129
+ await screen.findByTestId('dialog-inner')
130
+
131
+ // Open the select.
132
+ fireEvent.pointerDown(screen.getByTestId('select-trigger'), { button: 0, pointerType: 'mouse' })
133
+ await screen.findByTestId('select-content')
134
+
135
+ // A press on the dialog backdrop closes only the select; the dialog stays open.
136
+ pressBackdrop()
137
+ expect(screen.queryByTestId('select-content')).toBeNull()
138
+ expect(screen.queryByTestId('dialog-inner')).not.toBeNull()
139
+
140
+ // A second press on the backdrop then closes the dialog.
141
+ pressBackdrop()
142
+ expect(screen.queryByTestId('dialog-inner')).toBeNull()
143
+ })
144
+
145
+ it('dismisses a normal dialog on a backdrop press', async () => {
146
+ await renderWithDialog()
147
+
148
+ fireEvent.click(screen.getByTestId('open-dialog'))
149
+ await screen.findByTestId('dialog-inner')
150
+
151
+ pressBackdrop()
152
+ expect(screen.queryByTestId('dialog-inner')).toBeNull()
153
+ })
154
+
155
+ it('does not dismiss when a press starts inside the content and ends on the backdrop', async () => {
156
+ await renderWithDialog()
157
+
158
+ fireEvent.click(screen.getByTestId('open-dialog'))
159
+ await screen.findByTestId('dialog-inner')
160
+
161
+ // A press that starts inside the content (e.g. selecting text)...
162
+ fireEvent.pointerDown(screen.getByTestId('dialog-inner'), { pointerType: 'mouse' })
163
+ // ...and is released on the backdrop should not dismiss the dialog, since Radix
164
+ // dismisses on pointer-down and the press began inside the content.
165
+ const el = document.querySelector('.tlui-dialog__positioner')!
166
+ fireEvent.pointerUp(el, { pointerType: 'mouse' })
167
+ fireEvent.click(el)
168
+
169
+ expect(screen.queryByTestId('dialog-inner')).not.toBeNull()
170
+ })
171
+
172
+ it('dismisses a dialog on escape', async () => {
173
+ await renderWithDialog()
174
+
175
+ fireEvent.click(screen.getByTestId('open-dialog'))
176
+ await screen.findByTestId('dialog-inner')
177
+
178
+ fireEvent.keyDown(document.body, { key: 'Escape' })
179
+ expect(screen.queryByTestId('dialog-inner')).toBeNull()
180
+ })
181
+
182
+ it('escape dismisses one stacked layer at a time: the select first, then the dialog', async () => {
183
+ await renderWithDialog()
184
+
185
+ fireEvent.click(screen.getByTestId('open-dialog'))
186
+ await screen.findByTestId('dialog-inner')
187
+
188
+ fireEvent.pointerDown(screen.getByTestId('select-trigger'), { button: 0, pointerType: 'mouse' })
189
+ await screen.findByTestId('select-content')
190
+
191
+ // The first escape closes only the select (the topmost layer); the dialog stays.
192
+ fireEvent.keyDown(document.body, { key: 'Escape' })
193
+ expect(screen.queryByTestId('select-content')).toBeNull()
194
+ expect(screen.queryByTestId('dialog-inner')).not.toBeNull()
195
+
196
+ // The second escape then closes the dialog.
197
+ fireEvent.keyDown(document.body, { key: 'Escape' })
198
+ expect(screen.queryByTestId('dialog-inner')).toBeNull()
199
+ })
200
+
201
+ it('keeps a preventBackgroundClose dialog open on a backdrop press but still closes on escape', async () => {
202
+ await renderWithDialog({ preventBackgroundClose: true })
203
+
204
+ fireEvent.click(screen.getByTestId('open-dialog'))
205
+ await screen.findByTestId('dialog-inner')
206
+
207
+ // A backdrop press is ignored...
208
+ pressBackdrop()
209
+ expect(screen.queryByTestId('dialog-inner')).not.toBeNull()
210
+
211
+ // ...but escape still dismisses the dialog.
212
+ fireEvent.keyDown(document.body, { key: 'Escape' })
213
+ expect(screen.queryByTestId('dialog-inner')).toBeNull()
214
+ })
215
+
216
+ it('backdrop presses dismiss stacked dialogs one layer at a time', async () => {
217
+ await renderWithParentDialog()
218
+
219
+ fireEvent.click(screen.getByTestId('open-parent'))
220
+ await screen.findByTestId('parent-dialog')
221
+
222
+ fireEvent.click(screen.getByTestId('open-nested'))
223
+ await screen.findByTestId('nested-dialog')
224
+
225
+ // Opening the nested dialog (which stacks on top and grabs focus) must not dismiss
226
+ // the parent — the dialogs stack rather than collapsing together.
227
+ expect(screen.queryByTestId('parent-dialog')).not.toBeNull()
228
+
229
+ // A backdrop press closes only the topmost (nested) dialog; the parent stays open.
230
+ pressTopBackdrop()
231
+ expect(screen.queryByTestId('nested-dialog')).toBeNull()
232
+ expect(screen.queryByTestId('parent-dialog')).not.toBeNull()
233
+
234
+ // A second backdrop press then closes the parent.
235
+ pressTopBackdrop()
236
+ expect(screen.queryByTestId('parent-dialog')).toBeNull()
237
+ })
238
+
239
+ it('escape dismisses a nested dialog before its parent', async () => {
240
+ await renderWithParentDialog()
241
+
242
+ fireEvent.click(screen.getByTestId('open-parent'))
243
+ await screen.findByTestId('parent-dialog')
244
+
245
+ fireEvent.click(screen.getByTestId('open-nested'))
246
+ await screen.findByTestId('nested-dialog')
247
+
248
+ // The first escape closes only the nested dialog; the parent stays.
249
+ fireEvent.keyDown(document.body, { key: 'Escape' })
250
+ expect(screen.queryByTestId('nested-dialog')).toBeNull()
251
+ expect(screen.queryByTestId('parent-dialog')).not.toBeNull()
252
+
253
+ // The second escape then closes the parent.
254
+ fireEvent.keyDown(document.body, { key: 'Escape' })
255
+ expect(screen.queryByTestId('parent-dialog')).toBeNull()
256
+ })
@@ -0,0 +1,154 @@
1
+ import { act } from '@testing-library/react'
2
+ import { Box, DefaultColorStyle, Editor, 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
+ // Move the pointer so the editor's current page point is over a given location. The
17
+ // copy-hovered-styles action hit-tests this point, which also drills into groups.
18
+ function movePointerTo(editor: Editor, x: number, y: number) {
19
+ act(() => {
20
+ editor.dispatch({
21
+ type: 'pointer',
22
+ target: 'canvas',
23
+ name: 'pointer_move',
24
+ point: { x, y },
25
+ pointerId: 1,
26
+ button: 0,
27
+ isPen: false,
28
+ shiftKey: false,
29
+ altKey: false,
30
+ ctrlKey: false,
31
+ metaKey: false,
32
+ accelKey: false,
33
+ })
34
+ editor.emit('tick', 16)
35
+ })
36
+ }
37
+
38
+ async function setup() {
39
+ let actions: TLUiActionsContextType | null = null
40
+ const { editor } = await renderTldrawComponentWithEditor(
41
+ (onMount) => (
42
+ <Tldraw onMount={onMount}>
43
+ <ActionCapturer onCapture={(a) => (actions = a)} />
44
+ </Tldraw>
45
+ ),
46
+ { waitForPatterns: false }
47
+ )
48
+
49
+ // The action hit-tests rendering shapes, so give the editor a real viewport.
50
+ act(() => {
51
+ editor.updateViewportScreenBounds(new Box(0, 0, 1000, 800))
52
+ })
53
+
54
+ return { editor, copyHoveredStyles: actions!['copy-hovered-styles'] }
55
+ }
56
+
57
+ describe('copy-hovered-styles action', () => {
58
+ it('copies styles from the shape under the pointer in any tool idle state', async () => {
59
+ const { editor, copyHoveredStyles } = await setup()
60
+
61
+ const redId = createShapeId()
62
+ editor.createShapes([
63
+ {
64
+ id: redId,
65
+ type: 'geo',
66
+ x: 0,
67
+ y: 0,
68
+ props: { w: 100, h: 100, color: 'red', fill: 'solid' },
69
+ },
70
+ ])
71
+
72
+ // Point at the red shape.
73
+ movePointerTo(editor, 50, 50)
74
+
75
+ // From the select tool's idle state, the red shape's color is copied to the next shape.
76
+ editor.setStyleForNextShapes(DefaultColorStyle, 'black')
77
+ editor.setCurrentTool('select.idle')
78
+ expect(editor.getPath()).toBe('select.idle')
79
+ await copyHoveredStyles.onSelect('kbd')
80
+ expect(editor.getStyleForNextShape(DefaultColorStyle)).toBe('red')
81
+
82
+ // It also runs from another tool's idle state, like geo.idle.
83
+ editor.setStyleForNextShapes(DefaultColorStyle, 'black')
84
+ editor.setCurrentTool('geo')
85
+ expect(editor.getPath()).toBe('geo.idle')
86
+ await copyHoveredStyles.onSelect('kbd')
87
+ expect(editor.getStyleForNextShape(DefaultColorStyle)).toBe('red')
88
+ })
89
+
90
+ it('does nothing when the tool is not in an idle state', async () => {
91
+ const { editor, copyHoveredStyles } = await setup()
92
+
93
+ const redId = createShapeId()
94
+ editor.createShapes([
95
+ {
96
+ id: redId,
97
+ type: 'geo',
98
+ x: 0,
99
+ y: 0,
100
+ props: { w: 100, h: 100, color: 'red', fill: 'solid' },
101
+ },
102
+ ])
103
+
104
+ movePointerTo(editor, 50, 50)
105
+
106
+ // Brushing is not an idle state, so the action is a no-op.
107
+ editor.setStyleForNextShapes(DefaultColorStyle, 'black')
108
+ editor.setCurrentTool('select.brushing')
109
+ expect(editor.getPath()).toBe('select.brushing')
110
+ await copyHoveredStyles.onSelect('kbd')
111
+ expect(editor.getStyleForNextShape(DefaultColorStyle)).toBe('black')
112
+ })
113
+
114
+ it('copies styles from the child under the cursor when pointing inside a group', async () => {
115
+ const { editor, copyHoveredStyles } = await setup()
116
+
117
+ const redId = createShapeId()
118
+ const blueId = createShapeId()
119
+ editor.createShapes([
120
+ {
121
+ id: redId,
122
+ type: 'geo',
123
+ x: 0,
124
+ y: 0,
125
+ props: { w: 100, h: 100, color: 'red', fill: 'solid' },
126
+ },
127
+ {
128
+ id: blueId,
129
+ type: 'geo',
130
+ x: 200,
131
+ y: 0,
132
+ props: { w: 100, h: 100, color: 'blue', fill: 'solid' },
133
+ },
134
+ ])
135
+ editor.setSelectedShapes([redId, blueId])
136
+ editor.groupShapes(editor.getSelectedShapeIds())
137
+ const groupId = editor.getOnlySelectedShapeId()!
138
+ expect(editor.isShapeOfType(editor.getShape(groupId)!, 'group')).toBe(true)
139
+ editor.selectNone()
140
+ editor.setCurrentTool('select.idle')
141
+
142
+ // The whole group is highlighted, but the hit test drills into it: pointing at the
143
+ // red child copies red, and pointing at the blue child copies blue.
144
+ editor.setStyleForNextShapes(DefaultColorStyle, 'black')
145
+ movePointerTo(editor, 50, 50)
146
+ await copyHoveredStyles.onSelect('kbd')
147
+ expect(editor.getStyleForNextShape(DefaultColorStyle)).toBe('red')
148
+
149
+ editor.setStyleForNextShapes(DefaultColorStyle, 'black')
150
+ movePointerTo(editor, 250, 50)
151
+ await copyHoveredStyles.onSelect('kbd')
152
+ expect(editor.getStyleForNextShape(DefaultColorStyle)).toBe('blue')
153
+ })
154
+ })