tldraw 3.16.0-canary.5dac57cf9465 → 3.16.0-canary.614a556981b7

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 (171) hide show
  1. package/dist-cjs/index.d.ts +74 -4
  2. package/dist-cjs/index.js +5 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/canvas/TldrawScribble.js +1 -1
  5. package/dist-cjs/lib/canvas/TldrawScribble.js.map +2 -2
  6. package/dist-cjs/lib/shapes/arrow/ArrowShapeUtil.js +3 -3
  7. package/dist-cjs/lib/shapes/arrow/ArrowShapeUtil.js.map +2 -2
  8. package/dist-cjs/lib/shapes/arrow/elbow/ElbowArrowDebug.js +3 -3
  9. package/dist-cjs/lib/shapes/arrow/elbow/ElbowArrowDebug.js.map +1 -1
  10. package/dist-cjs/lib/shapes/draw/DrawShapeUtil.js +3 -3
  11. package/dist-cjs/lib/shapes/draw/DrawShapeUtil.js.map +2 -2
  12. package/dist-cjs/lib/shapes/embed/EmbedShapeUtil.js +1 -1
  13. package/dist-cjs/lib/shapes/embed/EmbedShapeUtil.js.map +1 -1
  14. package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js +12 -12
  15. package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js.map +2 -2
  16. package/dist-cjs/lib/shapes/frame/components/FrameHeading.js +1 -1
  17. package/dist-cjs/lib/shapes/frame/components/FrameHeading.js.map +2 -2
  18. package/dist-cjs/lib/shapes/geo/GeoShapeUtil.js +2 -2
  19. package/dist-cjs/lib/shapes/geo/GeoShapeUtil.js.map +2 -2
  20. package/dist-cjs/lib/shapes/geo/components/GeoShapeBody.js +2 -1
  21. package/dist-cjs/lib/shapes/geo/components/GeoShapeBody.js.map +2 -2
  22. package/dist-cjs/lib/shapes/highlight/HighlightShapeUtil.js +5 -1
  23. package/dist-cjs/lib/shapes/highlight/HighlightShapeUtil.js.map +2 -2
  24. package/dist-cjs/lib/shapes/image/ImageShapeUtil.js +3 -3
  25. package/dist-cjs/lib/shapes/image/ImageShapeUtil.js.map +1 -1
  26. package/dist-cjs/lib/shapes/line/LineShapeUtil.js +5 -1
  27. package/dist-cjs/lib/shapes/line/LineShapeUtil.js.map +2 -2
  28. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js +4 -4
  29. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js.map +2 -2
  30. package/dist-cjs/lib/shapes/shared/ShapeFill.js +5 -5
  31. package/dist-cjs/lib/shapes/shared/ShapeFill.js.map +2 -2
  32. package/dist-cjs/lib/shapes/text/TextShapeUtil.js +2 -2
  33. package/dist-cjs/lib/shapes/text/TextShapeUtil.js.map +2 -2
  34. package/dist-cjs/lib/shapes/video/VideoShapeUtil.js +3 -3
  35. package/dist-cjs/lib/shapes/video/VideoShapeUtil.js.map +1 -1
  36. package/dist-cjs/lib/tools/SelectTool/childStates/Translating.js.map +2 -2
  37. package/dist-cjs/lib/ui/TldrawUi.js +14 -0
  38. package/dist-cjs/lib/ui/TldrawUi.js.map +3 -3
  39. package/dist-cjs/lib/ui/components/ActionsMenu/DefaultActionsMenu.js +10 -2
  40. package/dist-cjs/lib/ui/components/ActionsMenu/DefaultActionsMenu.js.map +2 -2
  41. package/dist-cjs/lib/ui/components/Minimap/MinimapManager.js +4 -4
  42. package/dist-cjs/lib/ui/components/Minimap/MinimapManager.js.map +2 -2
  43. package/dist-cjs/lib/ui/components/MobileStylePanel.js +5 -3
  44. package/dist-cjs/lib/ui/components/MobileStylePanel.js.map +2 -2
  45. package/dist-cjs/lib/ui/components/Toolbar/DefaultImageToolbarContent.js +1 -1
  46. package/dist-cjs/lib/ui/components/Toolbar/DefaultImageToolbarContent.js.map +2 -2
  47. package/dist-cjs/lib/ui/components/Toolbar/DefaultToolbar.js +66 -22
  48. package/dist-cjs/lib/ui/components/Toolbar/DefaultToolbar.js.map +3 -3
  49. package/dist-cjs/lib/ui/components/Toolbar/OverflowingToolbar.js +188 -78
  50. package/dist-cjs/lib/ui/components/Toolbar/OverflowingToolbar.js.map +3 -3
  51. package/dist-cjs/lib/ui/components/primitives/TldrawUiButtonPicker.js +1 -1
  52. package/dist-cjs/lib/ui/components/primitives/TldrawUiButtonPicker.js.map +2 -2
  53. package/dist-cjs/lib/ui/components/primitives/TldrawUiToolbar.js +15 -3
  54. package/dist-cjs/lib/ui/components/primitives/TldrawUiToolbar.js.map +2 -2
  55. package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js +106 -82
  56. package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js.map +2 -2
  57. package/dist-cjs/lib/ui/components/primitives/layout.js +30 -5
  58. package/dist-cjs/lib/ui/components/primitives/layout.js.map +2 -2
  59. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuGroup.js +30 -7
  60. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuGroup.js.map +2 -2
  61. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js +152 -1
  62. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js.map +2 -2
  63. package/dist-cjs/lib/ui/context/events.js.map +2 -2
  64. package/dist-cjs/lib/ui/hooks/useTools.js +76 -9
  65. package/dist-cjs/lib/ui/hooks/useTools.js.map +2 -2
  66. package/dist-cjs/lib/ui/version.js +3 -3
  67. package/dist-cjs/lib/ui/version.js.map +1 -1
  68. package/dist-esm/index.d.mts +74 -4
  69. package/dist-esm/index.mjs +10 -2
  70. package/dist-esm/index.mjs.map +2 -2
  71. package/dist-esm/lib/canvas/TldrawScribble.mjs +1 -1
  72. package/dist-esm/lib/canvas/TldrawScribble.mjs.map +2 -2
  73. package/dist-esm/lib/shapes/arrow/ArrowShapeUtil.mjs +4 -3
  74. package/dist-esm/lib/shapes/arrow/ArrowShapeUtil.mjs.map +2 -2
  75. package/dist-esm/lib/shapes/arrow/elbow/ElbowArrowDebug.mjs +3 -3
  76. package/dist-esm/lib/shapes/arrow/elbow/ElbowArrowDebug.mjs.map +1 -1
  77. package/dist-esm/lib/shapes/draw/DrawShapeUtil.mjs +4 -3
  78. package/dist-esm/lib/shapes/draw/DrawShapeUtil.mjs.map +2 -2
  79. package/dist-esm/lib/shapes/embed/EmbedShapeUtil.mjs +1 -1
  80. package/dist-esm/lib/shapes/embed/EmbedShapeUtil.mjs.map +1 -1
  81. package/dist-esm/lib/shapes/frame/FrameShapeUtil.mjs +13 -12
  82. package/dist-esm/lib/shapes/frame/FrameShapeUtil.mjs.map +2 -2
  83. package/dist-esm/lib/shapes/frame/components/FrameHeading.mjs +1 -1
  84. package/dist-esm/lib/shapes/frame/components/FrameHeading.mjs.map +2 -2
  85. package/dist-esm/lib/shapes/geo/GeoShapeUtil.mjs +3 -2
  86. package/dist-esm/lib/shapes/geo/GeoShapeUtil.mjs.map +2 -2
  87. package/dist-esm/lib/shapes/geo/components/GeoShapeBody.mjs +2 -1
  88. package/dist-esm/lib/shapes/geo/components/GeoShapeBody.mjs.map +2 -2
  89. package/dist-esm/lib/shapes/highlight/HighlightShapeUtil.mjs +6 -1
  90. package/dist-esm/lib/shapes/highlight/HighlightShapeUtil.mjs.map +2 -2
  91. package/dist-esm/lib/shapes/image/ImageShapeUtil.mjs +3 -3
  92. package/dist-esm/lib/shapes/image/ImageShapeUtil.mjs.map +1 -1
  93. package/dist-esm/lib/shapes/line/LineShapeUtil.mjs +6 -1
  94. package/dist-esm/lib/shapes/line/LineShapeUtil.mjs.map +2 -2
  95. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs +5 -4
  96. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs.map +2 -2
  97. package/dist-esm/lib/shapes/shared/ShapeFill.mjs +6 -5
  98. package/dist-esm/lib/shapes/shared/ShapeFill.mjs.map +2 -2
  99. package/dist-esm/lib/shapes/text/TextShapeUtil.mjs +3 -2
  100. package/dist-esm/lib/shapes/text/TextShapeUtil.mjs.map +2 -2
  101. package/dist-esm/lib/shapes/video/VideoShapeUtil.mjs +3 -3
  102. package/dist-esm/lib/shapes/video/VideoShapeUtil.mjs.map +1 -1
  103. package/dist-esm/lib/tools/SelectTool/childStates/Translating.mjs.map +2 -2
  104. package/dist-esm/lib/ui/TldrawUi.mjs +16 -2
  105. package/dist-esm/lib/ui/TldrawUi.mjs.map +3 -3
  106. package/dist-esm/lib/ui/components/ActionsMenu/DefaultActionsMenu.mjs +10 -2
  107. package/dist-esm/lib/ui/components/ActionsMenu/DefaultActionsMenu.mjs.map +2 -2
  108. package/dist-esm/lib/ui/components/Minimap/MinimapManager.mjs +4 -4
  109. package/dist-esm/lib/ui/components/Minimap/MinimapManager.mjs.map +2 -2
  110. package/dist-esm/lib/ui/components/MobileStylePanel.mjs +6 -3
  111. package/dist-esm/lib/ui/components/MobileStylePanel.mjs.map +2 -2
  112. package/dist-esm/lib/ui/components/Toolbar/DefaultImageToolbarContent.mjs +1 -1
  113. package/dist-esm/lib/ui/components/Toolbar/DefaultImageToolbarContent.mjs.map +2 -2
  114. package/dist-esm/lib/ui/components/Toolbar/DefaultToolbar.mjs +56 -22
  115. package/dist-esm/lib/ui/components/Toolbar/DefaultToolbar.mjs.map +2 -2
  116. package/dist-esm/lib/ui/components/Toolbar/OverflowingToolbar.mjs +192 -80
  117. package/dist-esm/lib/ui/components/Toolbar/OverflowingToolbar.mjs.map +3 -3
  118. package/dist-esm/lib/ui/components/primitives/TldrawUiButtonPicker.mjs +2 -1
  119. package/dist-esm/lib/ui/components/primitives/TldrawUiButtonPicker.mjs.map +2 -2
  120. package/dist-esm/lib/ui/components/primitives/TldrawUiToolbar.mjs +16 -4
  121. package/dist-esm/lib/ui/components/primitives/TldrawUiToolbar.mjs.map +2 -2
  122. package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs +108 -84
  123. package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs.map +2 -2
  124. package/dist-esm/lib/ui/components/primitives/layout.mjs +31 -6
  125. package/dist-esm/lib/ui/components/primitives/layout.mjs.map +2 -2
  126. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuGroup.mjs +30 -7
  127. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuGroup.mjs.map +2 -2
  128. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs +160 -3
  129. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs.map +2 -2
  130. package/dist-esm/lib/ui/context/events.mjs.map +2 -2
  131. package/dist-esm/lib/ui/hooks/useTools.mjs +83 -10
  132. package/dist-esm/lib/ui/hooks/useTools.mjs.map +2 -2
  133. package/dist-esm/lib/ui/version.mjs +3 -3
  134. package/dist-esm/lib/ui/version.mjs.map +1 -1
  135. package/package.json +3 -3
  136. package/src/index.ts +7 -0
  137. package/src/lib/canvas/TldrawScribble.tsx +1 -1
  138. package/src/lib/shapes/arrow/ArrowShapeUtil.tsx +4 -3
  139. package/src/lib/shapes/arrow/elbow/ElbowArrowDebug.tsx +3 -3
  140. package/src/lib/shapes/draw/DrawShapeUtil.tsx +4 -3
  141. package/src/lib/shapes/embed/EmbedShapeUtil.tsx +1 -1
  142. package/src/lib/shapes/frame/FrameShapeUtil.tsx +13 -14
  143. package/src/lib/shapes/frame/components/FrameHeading.tsx +1 -1
  144. package/src/lib/shapes/geo/GeoShapeUtil.tsx +3 -2
  145. package/src/lib/shapes/geo/components/GeoShapeBody.tsx +2 -2
  146. package/src/lib/shapes/highlight/HighlightShapeUtil.tsx +7 -1
  147. package/src/lib/shapes/image/ImageShapeUtil.tsx +3 -3
  148. package/src/lib/shapes/line/LineShapeUtil.tsx +6 -1
  149. package/src/lib/shapes/note/NoteShapeUtil.tsx +9 -4
  150. package/src/lib/shapes/shared/ShapeFill.tsx +6 -5
  151. package/src/lib/shapes/text/TextShapeUtil.tsx +3 -2
  152. package/src/lib/shapes/video/VideoShapeUtil.tsx +3 -3
  153. package/src/lib/tools/SelectTool/childStates/Translating.ts +0 -1
  154. package/src/lib/ui/TldrawUi.tsx +17 -2
  155. package/src/lib/ui/components/ActionsMenu/DefaultActionsMenu.tsx +13 -2
  156. package/src/lib/ui/components/Minimap/MinimapManager.ts +4 -4
  157. package/src/lib/ui/components/MobileStylePanel.tsx +9 -6
  158. package/src/lib/ui/components/Toolbar/DefaultImageToolbarContent.tsx +1 -1
  159. package/src/lib/ui/components/Toolbar/DefaultToolbar.tsx +55 -24
  160. package/src/lib/ui/components/Toolbar/OverflowingToolbar.tsx +208 -56
  161. package/src/lib/ui/components/primitives/TldrawUiButtonPicker.tsx +3 -2
  162. package/src/lib/ui/components/primitives/TldrawUiToolbar.tsx +22 -5
  163. package/src/lib/ui/components/primitives/TldrawUiTooltip.tsx +112 -82
  164. package/src/lib/ui/components/primitives/layout.tsx +79 -5
  165. package/src/lib/ui/components/primitives/menus/TldrawUiMenuGroup.tsx +30 -7
  166. package/src/lib/ui/components/primitives/menus/TldrawUiMenuItem.tsx +218 -2
  167. package/src/lib/ui/context/events.tsx +1 -0
  168. package/src/lib/ui/hooks/useTools.tsx +118 -10
  169. package/src/lib/ui/version.ts +3 -3
  170. package/src/lib/ui.css +342 -238
  171. package/tldraw.css +635 -528
@@ -1,7 +1,8 @@
1
- import { Editor, uniqueId, useMaybeEditor, Vec } from '@tldraw/editor'
1
+ import { assert, Editor, uniqueId, useMaybeEditor, Vec } from '@tldraw/editor'
2
2
  import { Tooltip as _Tooltip } from 'radix-ui'
3
- import React, { createContext, useContext, useEffect, useRef, useState } from 'react'
3
+ import React, { createContext, forwardRef, useContext, useEffect, useRef, useState } from 'react'
4
4
  import { usePrefersReducedMotion } from '../../../shapes/shared/usePrefersReducedMotion'
5
+ import { useTldrawUiOrientation } from './layout'
5
6
 
6
7
  const DEFAULT_TOOLTIP_DELAY_MS = 700
7
8
 
@@ -69,20 +70,32 @@ class TooltipManager {
69
70
  this.notify()
70
71
  }
71
72
 
72
- hideTooltip(tooltipId: string) {
73
- // Only hide if this is the current tooltip
74
- if (this.currentTooltipId === tooltipId) {
75
- // Start destroy timeout (1 second)
76
- if (this.editor) {
77
- this.destroyTimeoutId = this.editor.timers.setTimeout(() => {
78
- this.currentTooltipId = null
79
- this.currentContent = ''
80
- this.activeElement = null
81
- this.destroyTimeoutId = null
82
- this.notify()
83
- }, 300)
73
+ hideTooltip(tooltipId: string, instant: boolean = false) {
74
+ const hide = () => {
75
+ // Only hide if this is the current tooltip
76
+ if (this.currentTooltipId === tooltipId) {
77
+ this.currentTooltipId = null
78
+ this.currentContent = ''
79
+ this.activeElement = null
80
+ this.destroyTimeoutId = null
81
+ this.notify()
84
82
  }
85
83
  }
84
+
85
+ if (instant) {
86
+ hide()
87
+ } else if (this.editor) {
88
+ // Start destroy timeout (1 second)
89
+ this.destroyTimeoutId = this.editor.timers.setTimeout(hide, 300)
90
+ }
91
+ }
92
+
93
+ hideAllTooltips() {
94
+ this.currentTooltipId = null
95
+ this.currentContent = ''
96
+ this.activeElement = null
97
+ this.destroyTimeoutId = null
98
+ this.notify()
86
99
  }
87
100
 
88
101
  getCurrentTooltipData() {
@@ -96,7 +109,7 @@ class TooltipManager {
96
109
  }
97
110
  }
98
111
 
99
- const tooltipManager = TooltipManager.getInstance()
112
+ export const tooltipManager = TooltipManager.getInstance()
100
113
 
101
114
  // Context for the tooltip singleton
102
115
  const TooltipSingletonContext = createContext<boolean>(false)
@@ -235,79 +248,96 @@ function TooltipSingleton() {
235
248
  }
236
249
 
237
250
  /** @public @react */
238
- export function TldrawUiTooltip({
239
- children,
240
- content,
241
- side = 'bottom',
242
- sideOffset = 5,
243
- disabled = false,
244
- }: TldrawUiTooltipProps) {
245
- const editor = useMaybeEditor()
246
- const tooltipId = useRef<string>(uniqueId())
247
- const hasProvider = useContext(TooltipSingletonContext)
251
+ export const TldrawUiTooltip = forwardRef<HTMLButtonElement, TldrawUiTooltipProps>(
252
+ ({ children, content, side, sideOffset = 5, disabled = false }, ref) => {
253
+ const editor = useMaybeEditor()
254
+ const tooltipId = useRef<string>(uniqueId())
255
+ const hasProvider = useContext(TooltipSingletonContext)
256
+
257
+ const orientationCtx = useTldrawUiOrientation()
258
+ const sideToUse = side ?? orientationCtx.tooltipSide
259
+
260
+ useEffect(() => {
261
+ const currentTooltipId = tooltipId.current
262
+ return () => {
263
+ if (hasProvider) {
264
+ tooltipManager.hideTooltip(currentTooltipId, true)
265
+ }
266
+ }
267
+ }, [hasProvider])
248
268
 
249
- // Don't show tooltip if disabled, no content, or UI labels are disabled
250
- if (disabled || !content) {
251
- return <>{children}</>
252
- }
269
+ // Don't show tooltip if disabled, no content, or UI labels are disabled
270
+ if (disabled || !content) {
271
+ return <>{children}</>
272
+ }
253
273
 
254
- // Fallback to old behavior if no provider
255
- if (!hasProvider) {
256
- return (
257
- <_Tooltip.Root
258
- delayDuration={editor?.options.tooltipDelayMs || DEFAULT_TOOLTIP_DELAY_MS}
259
- disableHoverableContent
260
- >
261
- <_Tooltip.Trigger asChild>{children}</_Tooltip.Trigger>
262
- <_Tooltip.Content
263
- className="tlui-tooltip"
264
- side={side}
265
- sideOffset={sideOffset}
266
- avoidCollisions
267
- collisionPadding={8}
268
- dir="ltr"
274
+ // Fallback to old behavior if no provider
275
+ if (!hasProvider) {
276
+ return (
277
+ <_Tooltip.Root
278
+ delayDuration={editor?.options.tooltipDelayMs || DEFAULT_TOOLTIP_DELAY_MS}
279
+ disableHoverableContent
269
280
  >
270
- {content}
271
- <_Tooltip.Arrow className="tlui-tooltip__arrow" />
272
- </_Tooltip.Content>
273
- </_Tooltip.Root>
274
- )
275
- }
281
+ <_Tooltip.Trigger asChild ref={ref}>
282
+ {children}
283
+ </_Tooltip.Trigger>
284
+ <_Tooltip.Content
285
+ className="tlui-tooltip"
286
+ side={sideToUse}
287
+ sideOffset={sideOffset}
288
+ avoidCollisions
289
+ collisionPadding={8}
290
+ dir="ltr"
291
+ >
292
+ {content}
293
+ <_Tooltip.Arrow className="tlui-tooltip__arrow" />
294
+ </_Tooltip.Content>
295
+ </_Tooltip.Root>
296
+ )
297
+ }
276
298
 
277
- const handleMouseEnter = (event: React.MouseEvent<HTMLElement>) => {
278
- tooltipManager.showTooltip(
279
- tooltipId.current,
280
- content,
281
- event.currentTarget as HTMLElement,
282
- side,
283
- sideOffset
284
- )
285
- }
299
+ const child = React.Children.only(children)
300
+ assert(React.isValidElement(child), 'TldrawUiTooltip children must be a single element')
301
+
302
+ const handleMouseEnter = (event: React.MouseEvent<HTMLElement>) => {
303
+ child.props.onMouseEnter?.(event)
304
+ tooltipManager.showTooltip(
305
+ tooltipId.current,
306
+ content,
307
+ event.currentTarget as HTMLElement,
308
+ sideToUse,
309
+ sideOffset
310
+ )
311
+ }
286
312
 
287
- const handleMouseLeave = () => {
288
- tooltipManager.hideTooltip(tooltipId.current)
289
- }
313
+ const handleMouseLeave = (event: React.MouseEvent<HTMLElement>) => {
314
+ child.props.onMouseLeave?.(event)
315
+ tooltipManager.hideTooltip(tooltipId.current)
316
+ }
290
317
 
291
- const handleFocus = (event: React.FocusEvent<HTMLElement>) => {
292
- tooltipManager.showTooltip(
293
- tooltipId.current,
294
- content,
295
- event.currentTarget as HTMLElement,
296
- side,
297
- sideOffset
298
- )
299
- }
318
+ const handleFocus = (event: React.FocusEvent<HTMLElement>) => {
319
+ child.props.onFocus?.(event)
320
+ tooltipManager.showTooltip(
321
+ tooltipId.current,
322
+ content,
323
+ event.currentTarget as HTMLElement,
324
+ sideToUse,
325
+ sideOffset
326
+ )
327
+ }
300
328
 
301
- const handleBlur = () => {
302
- tooltipManager.hideTooltip(tooltipId.current)
303
- }
329
+ const handleBlur = (event: React.FocusEvent<HTMLElement>) => {
330
+ child.props.onBlur?.(event)
331
+ tooltipManager.hideTooltip(tooltipId.current)
332
+ }
304
333
 
305
- const childrenWithHandlers = React.cloneElement(children as React.ReactElement, {
306
- onMouseEnter: handleMouseEnter,
307
- onMouseLeave: handleMouseLeave,
308
- onFocus: handleFocus,
309
- onBlur: handleBlur,
310
- })
334
+ const childrenWithHandlers = React.cloneElement(children as React.ReactElement, {
335
+ onMouseEnter: handleMouseEnter,
336
+ onMouseLeave: handleMouseLeave,
337
+ onFocus: handleFocus,
338
+ onBlur: handleBlur,
339
+ })
311
340
 
312
- return childrenWithHandlers
313
- }
341
+ return childrenWithHandlers
342
+ }
343
+ )
@@ -1,10 +1,60 @@
1
1
  import classNames from 'classnames'
2
2
  import { Slot } from 'radix-ui'
3
- import { HTMLAttributes, ReactNode, forwardRef } from 'react'
3
+ import { HTMLAttributes, ReactNode, createContext, forwardRef, useContext } from 'react'
4
+
5
+ /** @public */
6
+ export interface TldrawUiOrientationContext {
7
+ orientation: 'horizontal' | 'vertical'
8
+ tooltipSide: 'top' | 'right' | 'bottom' | 'left'
9
+ }
10
+
11
+ const TldrawUiOrientationContext = createContext<TldrawUiOrientationContext>({
12
+ orientation: 'horizontal',
13
+ tooltipSide: 'bottom',
14
+ })
15
+
16
+ /** @public */
17
+ export interface TldrawUiOrientationProviderProps {
18
+ children: ReactNode
19
+ orientation: 'horizontal' | 'vertical'
20
+ tooltipSide?: 'top' | 'right' | 'bottom' | 'left'
21
+ }
22
+ /** @public @react */
23
+ export function TldrawUiOrientationProvider({
24
+ children,
25
+ orientation,
26
+ tooltipSide,
27
+ }: TldrawUiOrientationProviderProps) {
28
+ const prevContext = useTldrawUiOrientation()
29
+ // generally, we want tooltip side to cascade down through the layout - apart from when the
30
+ // orientation changes. If the tooltip side is "bottom", and then I include some vertical layout
31
+ // elements, keeping the tooltip side as bottom will cause the tooltip to overlap elements
32
+ // stacked on top of each other. In the absence of a tooltip side, we pick a default side based
33
+ // on the orientation whenever the orientation changes.
34
+ const tooltipSideToUse =
35
+ tooltipSide ??
36
+ (orientation === prevContext.orientation
37
+ ? prevContext.tooltipSide
38
+ : orientation === 'horizontal'
39
+ ? 'bottom'
40
+ : 'right')
41
+
42
+ return (
43
+ <TldrawUiOrientationContext.Provider value={{ orientation, tooltipSide: tooltipSideToUse }}>
44
+ {children}
45
+ </TldrawUiOrientationContext.Provider>
46
+ )
47
+ }
48
+
49
+ /** @public */
50
+ export function useTldrawUiOrientation() {
51
+ return useContext(TldrawUiOrientationContext)
52
+ }
4
53
 
5
54
  /** @public */
6
55
  export interface TLUiLayoutProps extends HTMLAttributes<HTMLDivElement> {
7
56
  children: ReactNode
57
+ tooltipSide?: 'top' | 'right' | 'bottom' | 'left'
8
58
  asChild?: boolean
9
59
  }
10
60
 
@@ -14,9 +64,29 @@ export interface TLUiLayoutProps extends HTMLAttributes<HTMLDivElement> {
14
64
  * @public @react
15
65
  */
16
66
  export const TldrawUiRow = forwardRef<HTMLDivElement, TLUiLayoutProps>(
17
- ({ asChild, className, ...props }, ref) => {
67
+ ({ asChild, className, tooltipSide, ...props }, ref) => {
68
+ const Component = asChild ? Slot.Root : 'div'
69
+ return (
70
+ <TldrawUiOrientationProvider orientation="horizontal" tooltipSide={tooltipSide}>
71
+ <Component ref={ref} className={classNames('tlui-row', className)} {...props} />
72
+ </TldrawUiOrientationProvider>
73
+ )
74
+ }
75
+ )
76
+
77
+ /**
78
+ * A column, usually of UI controls like buttons, select dropdown, checkboxes, etc.
79
+ *
80
+ * @public @react
81
+ */
82
+ export const TldrawUiColumn = forwardRef<HTMLDivElement, TLUiLayoutProps>(
83
+ ({ asChild, className, tooltipSide, ...props }, ref) => {
18
84
  const Component = asChild ? Slot.Root : 'div'
19
- return <Component ref={ref} className={classNames('tlui-row', className)} {...props} />
85
+ return (
86
+ <TldrawUiOrientationProvider orientation="vertical" tooltipSide={tooltipSide}>
87
+ <Component ref={ref} className={classNames('tlui-column', className)} {...props} />
88
+ </TldrawUiOrientationProvider>
89
+ )
20
90
  }
21
91
  )
22
92
 
@@ -26,8 +96,12 @@ export const TldrawUiRow = forwardRef<HTMLDivElement, TLUiLayoutProps>(
26
96
  *
27
97
  * @public @react */
28
98
  export const TldrawUiGrid = forwardRef<HTMLDivElement, TLUiLayoutProps>(
29
- ({ asChild, className, ...props }, ref) => {
99
+ ({ asChild, className, tooltipSide, ...props }, ref) => {
30
100
  const Component = asChild ? Slot.Root : 'div'
31
- return <Component ref={ref} className={classNames('tlui-grid', className)} {...props} />
101
+ return (
102
+ <TldrawUiOrientationProvider orientation="horizontal" tooltipSide={tooltipSide}>
103
+ <Component ref={ref} className={classNames('tlui-grid', className)} {...props} />
104
+ </TldrawUiOrientationProvider>
105
+ )
32
106
  }
33
107
  )
@@ -3,6 +3,7 @@ import { ReactNode } from 'react'
3
3
  import { unwrapLabel } from '../../../context/actions'
4
4
  import { TLUiTranslationKey } from '../../../hooks/useTranslation/TLUiTranslationKey'
5
5
  import { useTranslation } from '../../../hooks/useTranslation/useTranslation'
6
+ import { TldrawUiColumn, TldrawUiGrid, TldrawUiRow, useTldrawUiOrientation } from '../layout'
6
7
  import { TldrawUiDropdownMenuGroup } from '../TldrawUiDropdownMenu'
7
8
  import { useTldrawUiMenuContext } from './TldrawUiMenuContext'
8
9
 
@@ -19,17 +20,18 @@ export interface TLUiMenuGroupProps<TranslationKey extends string = string> {
19
20
 
20
21
  /** @public @react */
21
22
  export function TldrawUiMenuGroup({ id, label, className, children }: TLUiMenuGroupProps) {
22
- const { type: menuType, sourceId } = useTldrawUiMenuContext()
23
+ const menu = useTldrawUiMenuContext()
24
+ const { orientation } = useTldrawUiOrientation()
23
25
  const msg = useTranslation()
24
- const labelToUse = unwrapLabel(label, menuType)
26
+ const labelToUse = unwrapLabel(label, menu.type)
25
27
  const labelStr = labelToUse ? msg(labelToUse as TLUiTranslationKey) : undefined
26
28
 
27
- switch (menuType) {
29
+ switch (menu.type) {
28
30
  case 'panel': {
29
31
  return (
30
32
  <div
31
33
  className={classNames('tlui-menu__group', className)}
32
- data-testid={`${sourceId}-group.${id}`}
34
+ data-testid={`${menu.sourceId}-group.${id}`}
33
35
  >
34
36
  {children}
35
37
  </div>
@@ -37,7 +39,10 @@ export function TldrawUiMenuGroup({ id, label, className, children }: TLUiMenuGr
37
39
  }
38
40
  case 'menu': {
39
41
  return (
40
- <TldrawUiDropdownMenuGroup className={className} data-testid={`${sourceId}-group.${id}`}>
42
+ <TldrawUiDropdownMenuGroup
43
+ className={className}
44
+ data-testid={`${menu.sourceId}-group.${id}`}
45
+ >
41
46
  {children}
42
47
  </TldrawUiDropdownMenuGroup>
43
48
  )
@@ -47,7 +52,7 @@ export function TldrawUiMenuGroup({ id, label, className, children }: TLUiMenuGr
47
52
  <div
48
53
  dir="ltr"
49
54
  className={classNames('tlui-menu__group', className)}
50
- data-testid={`${sourceId}-group.${id}`}
55
+ data-testid={`${menu.sourceId}-group.${id}`}
51
56
  >
52
57
  {children}
53
58
  </div>
@@ -56,12 +61,30 @@ export function TldrawUiMenuGroup({ id, label, className, children }: TLUiMenuGr
56
61
  case 'keyboard-shortcuts': {
57
62
  // todo: if groups need a label, let's give em a label
58
63
  return (
59
- <div className="tlui-shortcuts-dialog__group" data-testid={`${sourceId}-group.${id}`}>
64
+ <div className="tlui-shortcuts-dialog__group" data-testid={`${menu.sourceId}-group.${id}`}>
60
65
  <h2 className="tlui-shortcuts-dialog__group__title">{labelStr}</h2>
61
66
  <div className="tlui-shortcuts-dialog__group__content">{children}</div>
62
67
  </div>
63
68
  )
64
69
  }
70
+ case 'toolbar': {
71
+ const Layout = orientation === 'horizontal' ? TldrawUiRow : TldrawUiColumn
72
+ return (
73
+ <Layout className="tlui-main-toolbar__group" data-testid={`${menu.sourceId}-group.${id}`}>
74
+ {children}
75
+ </Layout>
76
+ )
77
+ }
78
+ case 'toolbar-overflow': {
79
+ return (
80
+ <TldrawUiGrid
81
+ className="tlui-main-toolbar__group"
82
+ data-testid={`${menu.sourceId}-group.${id}`}
83
+ >
84
+ {children}
85
+ </TldrawUiGrid>
86
+ )
87
+ }
65
88
  default: {
66
89
  return children
67
90
  }
@@ -1,9 +1,18 @@
1
- import { exhaustiveSwitchError, preventDefault } from '@tldraw/editor'
1
+ import {
2
+ exhaustiveSwitchError,
3
+ getPointerInfo,
4
+ preventDefault,
5
+ TLPointerEventInfo,
6
+ useEditor,
7
+ Vec,
8
+ VecModel,
9
+ } from '@tldraw/editor'
2
10
  import { ContextMenu as _ContextMenu } from 'radix-ui'
3
- import { useState } from 'react'
11
+ import { useMemo, useState } from 'react'
4
12
  import { unwrapLabel } from '../../../context/actions'
5
13
  import { TLUiEventSource } from '../../../context/events'
6
14
  import { useReadonly } from '../../../hooks/useReadonly'
15
+ import { TLUiToolItem } from '../../../hooks/useTools'
7
16
  import { TLUiTranslationKey } from '../../../hooks/useTranslation/TLUiTranslationKey'
8
17
  import { useTranslation } from '../../../hooks/useTranslation/useTranslation'
9
18
  import { kbdStr } from '../../../kbd-utils'
@@ -15,6 +24,7 @@ import { TldrawUiDropdownMenuItem } from '../TldrawUiDropdownMenu'
15
24
  import { TLUiIconJsx } from '../TldrawUiIcon'
16
25
  import { TldrawUiKbd } from '../TldrawUiKbd'
17
26
  import { TldrawUiToolbarButton } from '../TldrawUiToolbar'
27
+ import { tooltipManager } from '../TldrawUiTooltip'
18
28
  import { useTldrawUiMenuContext } from './TldrawUiMenuContext'
19
29
 
20
30
  /** @public */
@@ -63,6 +73,10 @@ export interface TLUiMenuItemProps<
63
73
  * Whether the item is selected.
64
74
  */
65
75
  isSelected?: boolean
76
+ /**
77
+ * The function to call when the item is dragged. If this is provided, the item will be draggable.
78
+ */
79
+ onDragStart?(source: TLUiEventSource, info: TLPointerEventInfo): void
66
80
  }
67
81
 
68
82
  /** @public @react */
@@ -81,6 +95,7 @@ export function TldrawUiMenuItem<
81
95
  onSelect,
82
96
  noClose,
83
97
  isSelected,
98
+ onDragStart,
84
99
  }: TLUiMenuItemProps<TranslationKey, IconType>) {
85
100
  const { type: menuType, sourceId } = useTldrawUiMenuContext()
86
101
 
@@ -207,6 +222,20 @@ export function TldrawUiMenuItem<
207
222
  )
208
223
  }
209
224
  case 'toolbar': {
225
+ if (onDragStart) {
226
+ return (
227
+ <DraggableToolbarButton
228
+ id={id}
229
+ icon={icon}
230
+ onSelect={onSelect}
231
+ onDragStart={onDragStart}
232
+ labelToUse={labelToUse}
233
+ titleStr={titleStr}
234
+ disabled={disabled}
235
+ isSelected={isSelected}
236
+ />
237
+ )
238
+ }
210
239
  return (
211
240
  <TldrawUiToolbarButton
212
241
  aria-label={labelStr}
@@ -227,6 +256,21 @@ export function TldrawUiMenuItem<
227
256
  )
228
257
  }
229
258
  case 'toolbar-overflow': {
259
+ if (onDragStart) {
260
+ return (
261
+ <DraggableToolbarButton
262
+ id={id}
263
+ icon={icon}
264
+ onSelect={onSelect}
265
+ onDragStart={onDragStart}
266
+ labelToUse={labelToUse}
267
+ titleStr={titleStr}
268
+ disabled={disabled}
269
+ isSelected={isSelected}
270
+ overflow
271
+ />
272
+ )
273
+ }
230
274
  return (
231
275
  <TldrawUiToolbarButton
232
276
  aria-label={labelStr}
@@ -248,3 +292,175 @@ export function TldrawUiMenuItem<
248
292
  }
249
293
  }
250
294
  }
295
+
296
+ function useDraggableEvents(
297
+ onDragStart: TLUiToolItem['onDragStart'],
298
+ onSelect: TLUiToolItem['onSelect']
299
+ ) {
300
+ const editor = useEditor()
301
+ const events = useMemo(() => {
302
+ let state = { name: 'idle' } as
303
+ | {
304
+ name: 'idle'
305
+ }
306
+ | {
307
+ name: 'pointing'
308
+ screenSpaceStart: VecModel
309
+ }
310
+ | {
311
+ name: 'dragging'
312
+ screenSpaceStart: VecModel
313
+ }
314
+ | {
315
+ name: 'dragged'
316
+ }
317
+
318
+ function handlePointerDown(e: React.PointerEvent<HTMLButtonElement>) {
319
+ state = {
320
+ name: 'pointing',
321
+ screenSpaceStart: { x: e.clientX, y: e.clientY },
322
+ }
323
+
324
+ e.currentTarget.setPointerCapture(e.pointerId)
325
+ }
326
+
327
+ function handlePointerMove(e: React.PointerEvent<HTMLButtonElement>) {
328
+ if ((e as any).isSpecialRedispatchedEvent) return
329
+
330
+ if (state.name === 'pointing') {
331
+ const distanceSq = Vec.Dist2(state.screenSpaceStart, { x: e.clientX, y: e.clientY })
332
+ if (
333
+ distanceSq >
334
+ (editor.getInstanceState().isCoarsePointer
335
+ ? editor.options.coarseDragDistanceSquared
336
+ : editor.options.dragDistanceSquared)
337
+ ) {
338
+ const screenSpaceStart = state.screenSpaceStart
339
+ state = {
340
+ name: 'dragging',
341
+ screenSpaceStart,
342
+ }
343
+
344
+ editor.run(() => {
345
+ // Set origin point
346
+ editor.dispatch({
347
+ type: 'pointer',
348
+ target: 'canvas',
349
+ name: 'pointer_down',
350
+ ...getPointerInfo(e),
351
+ point: screenSpaceStart,
352
+ })
353
+
354
+ // Pointer down potentially selects shapes, so we need to deselect them.
355
+ editor.selectNone()
356
+
357
+ // start drag
358
+ onDragStart?.('toolbar', {
359
+ type: 'pointer',
360
+ target: 'canvas',
361
+ name: 'pointer_move',
362
+ ...getPointerInfo(e),
363
+ point: screenSpaceStart,
364
+ })
365
+
366
+ tooltipManager.hideAllTooltips()
367
+ })
368
+ }
369
+ }
370
+ }
371
+
372
+ function handlePointerUp(e: React.PointerEvent<HTMLButtonElement>) {
373
+ if ((e as any).isSpecialRedispatchedEvent) return
374
+
375
+ e.currentTarget.releasePointerCapture(e.pointerId)
376
+
377
+ editor.dispatch({
378
+ type: 'pointer',
379
+ target: 'canvas',
380
+ name: 'pointer_up',
381
+ ...getPointerInfo(e),
382
+ })
383
+ }
384
+
385
+ function handleClick() {
386
+ if (state.name === 'dragging' || state.name === 'dragged') {
387
+ state = { name: 'idle' }
388
+ return true
389
+ }
390
+
391
+ state = { name: 'idle' }
392
+ onSelect?.('toolbar')
393
+ }
394
+
395
+ return {
396
+ onPointerDown: handlePointerDown,
397
+ onPointerMove: handlePointerMove,
398
+ onPointerUp: handlePointerUp,
399
+ onClick: handleClick,
400
+ }
401
+ }, [onDragStart, editor, onSelect])
402
+
403
+ return events
404
+ }
405
+
406
+ function DraggableToolbarButton({
407
+ id,
408
+ labelToUse,
409
+ titleStr,
410
+ disabled,
411
+ isSelected,
412
+ icon,
413
+ onSelect,
414
+ onDragStart,
415
+ overflow,
416
+ }: {
417
+ id: string
418
+ disabled: boolean
419
+ labelToUse?: string
420
+ titleStr?: string
421
+ isSelected?: boolean
422
+ icon: TLUiMenuItemProps['icon']
423
+ onSelect: TLUiMenuItemProps['onSelect']
424
+ onDragStart: TLUiMenuItemProps['onDragStart']
425
+ overflow?: boolean
426
+ }) {
427
+ const events = useDraggableEvents(onDragStart, onSelect)
428
+
429
+ if (overflow) {
430
+ return (
431
+ <TldrawUiToolbarButton
432
+ aria-label={labelToUse}
433
+ aria-pressed={isSelected ? 'true' : 'false'}
434
+ isActive={isSelected}
435
+ className="tlui-button-grid__button"
436
+ data-testid={`tools.more.${id}`}
437
+ data-value={id}
438
+ disabled={disabled}
439
+ title={titleStr}
440
+ type="icon"
441
+ {...events}
442
+ >
443
+ <TldrawUiButtonIcon icon={icon!} />
444
+ </TldrawUiToolbarButton>
445
+ )
446
+ }
447
+
448
+ return (
449
+ <TldrawUiToolbarButton
450
+ aria-label={labelToUse}
451
+ aria-pressed={isSelected ? 'true' : 'false'}
452
+ data-testid={`tools.${id}`}
453
+ data-value={id}
454
+ disabled={disabled}
455
+ onTouchStart={(e) => {
456
+ preventDefault(e)
457
+ onSelect('toolbar')
458
+ }}
459
+ title={titleStr}
460
+ type="tool"
461
+ {...events}
462
+ >
463
+ <TldrawUiButtonIcon icon={icon!} />
464
+ </TldrawUiToolbarButton>
465
+ )
466
+ }
@@ -127,6 +127,7 @@ export interface TLUiEventMap {
127
127
  'open-context-menu': null
128
128
  'adjust-shape-styles': null
129
129
  'copy-link': null
130
+ 'drag-tool': { id: string }
130
131
  'image-replace': null
131
132
  'video-replace': null
132
133
  'open-kbd-shortcuts': null