tldraw 3.16.0-next.4337ae1ab96d → 3.16.0-next.8eb6d5c2d8f4

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 (224) hide show
  1. package/dist-cjs/index.d.ts +100 -14
  2. package/dist-cjs/index.js +6 -2
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/defaultExternalContentHandlers.js +10 -0
  5. package/dist-cjs/lib/defaultExternalContentHandlers.js.map +2 -2
  6. package/dist-cjs/lib/shapes/arrow/arrow-types.js.map +1 -1
  7. package/dist-cjs/lib/shapes/arrow/arrowTargetState.js +3 -2
  8. package/dist-cjs/lib/shapes/arrow/arrowTargetState.js.map +2 -2
  9. package/dist-cjs/lib/shapes/arrow/toolStates/Pointing.js +1 -1
  10. package/dist-cjs/lib/shapes/arrow/toolStates/Pointing.js.map +2 -2
  11. package/dist-cjs/lib/shapes/bookmark/BookmarkShapeUtil.js +4 -4
  12. package/dist-cjs/lib/shapes/bookmark/BookmarkShapeUtil.js.map +2 -2
  13. package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js +2 -1
  14. package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js.map +2 -2
  15. package/dist-cjs/lib/shapes/frame/components/FrameLabelInput.js +8 -2
  16. package/dist-cjs/lib/shapes/frame/components/FrameLabelInput.js.map +2 -2
  17. package/dist-cjs/lib/shapes/geo/GeoShapeUtil.js +1 -0
  18. package/dist-cjs/lib/shapes/geo/GeoShapeUtil.js.map +2 -2
  19. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js +2 -1
  20. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js.map +2 -2
  21. package/dist-cjs/lib/shapes/shared/HyperlinkButton.js +4 -4
  22. package/dist-cjs/lib/shapes/shared/HyperlinkButton.js.map +2 -2
  23. package/dist-cjs/lib/shapes/shared/useEditablePlainText.js +3 -3
  24. package/dist-cjs/lib/shapes/shared/useEditablePlainText.js.map +2 -2
  25. package/dist-cjs/lib/shapes/text/PlainTextArea.js +3 -2
  26. package/dist-cjs/lib/shapes/text/PlainTextArea.js.map +2 -2
  27. package/dist-cjs/lib/shapes/text/RichTextArea.js +3 -3
  28. package/dist-cjs/lib/shapes/text/RichTextArea.js.map +2 -2
  29. package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js +3 -1
  30. package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js.map +2 -2
  31. package/dist-cjs/lib/ui/components/A11y.js +1 -1
  32. package/dist-cjs/lib/ui/components/A11y.js.map +2 -2
  33. package/dist-cjs/lib/ui/components/AccessibilityMenu.js +1 -1
  34. package/dist-cjs/lib/ui/components/AccessibilityMenu.js.map +2 -2
  35. package/dist-cjs/lib/ui/components/InputModeMenu.js +77 -0
  36. package/dist-cjs/lib/ui/components/InputModeMenu.js.map +7 -0
  37. package/dist-cjs/lib/ui/components/LanguageMenu.js +1 -0
  38. package/dist-cjs/lib/ui/components/LanguageMenu.js.map +2 -2
  39. package/dist-cjs/lib/ui/components/MainMenu/DefaultMainMenuContent.js +2 -0
  40. package/dist-cjs/lib/ui/components/MainMenu/DefaultMainMenuContent.js.map +2 -2
  41. package/dist-cjs/lib/ui/components/Minimap/DefaultMinimap.js +2 -1
  42. package/dist-cjs/lib/ui/components/Minimap/DefaultMinimap.js.map +2 -2
  43. package/dist-cjs/lib/ui/components/PageMenu/DefaultPageMenu.js +1 -1
  44. package/dist-cjs/lib/ui/components/PageMenu/DefaultPageMenu.js.map +2 -2
  45. package/dist-cjs/lib/ui/components/StylePanel/DefaultStylePanel.js +4 -2
  46. package/dist-cjs/lib/ui/components/StylePanel/DefaultStylePanel.js.map +2 -2
  47. package/dist-cjs/lib/ui/components/StylePanel/DefaultStylePanelContent.js +2 -2
  48. package/dist-cjs/lib/ui/components/StylePanel/DefaultStylePanelContent.js.map +2 -2
  49. package/dist-cjs/lib/ui/components/StylePanel/StylePanelButtonPicker.js +12 -3
  50. package/dist-cjs/lib/ui/components/StylePanel/StylePanelButtonPicker.js.map +2 -2
  51. package/dist-cjs/lib/ui/components/StylePanel/StylePanelContext.js +4 -2
  52. package/dist-cjs/lib/ui/components/StylePanel/StylePanelContext.js.map +2 -2
  53. package/dist-cjs/lib/ui/components/Toolbar/AltTextEditor.js +3 -2
  54. package/dist-cjs/lib/ui/components/Toolbar/AltTextEditor.js.map +2 -2
  55. package/dist-cjs/lib/ui/components/Toolbar/LinkEditor.js +5 -4
  56. package/dist-cjs/lib/ui/components/Toolbar/LinkEditor.js.map +2 -2
  57. package/dist-cjs/lib/ui/components/Toolbar/OverflowingToolbar.js +1 -1
  58. package/dist-cjs/lib/ui/components/Toolbar/OverflowingToolbar.js.map +2 -2
  59. package/dist-cjs/lib/ui/components/Toolbar/ToggleToolLockedButton.js +6 -2
  60. package/dist-cjs/lib/ui/components/Toolbar/ToggleToolLockedButton.js.map +2 -2
  61. package/dist-cjs/lib/ui/components/menu-items.js +6 -4
  62. package/dist-cjs/lib/ui/components/menu-items.js.map +2 -2
  63. package/dist-cjs/lib/ui/components/primitives/TldrawUiContextualToolbar.js +1 -1
  64. package/dist-cjs/lib/ui/components/primitives/TldrawUiContextualToolbar.js.map +2 -2
  65. package/dist-cjs/lib/ui/components/primitives/TldrawUiInput.js +5 -3
  66. package/dist-cjs/lib/ui/components/primitives/TldrawUiInput.js.map +2 -2
  67. package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js +1 -1
  68. package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js.map +2 -2
  69. package/dist-cjs/lib/ui/components/primitives/TldrawUiToolbar.js +1 -0
  70. package/dist-cjs/lib/ui/components/primitives/TldrawUiToolbar.js.map +2 -2
  71. package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js +68 -20
  72. package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js.map +2 -2
  73. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuCheckboxItem.js +3 -0
  74. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuCheckboxItem.js.map +2 -2
  75. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js +8 -8
  76. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js.map +2 -2
  77. package/dist-cjs/lib/ui/context/actions.js +11 -25
  78. package/dist-cjs/lib/ui/context/actions.js.map +2 -2
  79. package/dist-cjs/lib/ui/context/events.js.map +2 -2
  80. package/dist-cjs/lib/ui/hooks/useClipboardEvents.js +1 -1
  81. package/dist-cjs/lib/ui/hooks/useClipboardEvents.js.map +2 -2
  82. package/dist-cjs/lib/ui/hooks/useTranslation/TLUiTranslationKey.js.map +1 -1
  83. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js +11 -3
  84. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js.map +2 -2
  85. package/dist-cjs/lib/ui/version.js +3 -3
  86. package/dist-cjs/lib/ui/version.js.map +1 -1
  87. package/dist-esm/index.d.mts +100 -14
  88. package/dist-esm/index.mjs +11 -3
  89. package/dist-esm/index.mjs.map +2 -2
  90. package/dist-esm/lib/defaultExternalContentHandlers.mjs +10 -0
  91. package/dist-esm/lib/defaultExternalContentHandlers.mjs.map +2 -2
  92. package/dist-esm/lib/shapes/arrow/arrowTargetState.mjs +3 -2
  93. package/dist-esm/lib/shapes/arrow/arrowTargetState.mjs.map +2 -2
  94. package/dist-esm/lib/shapes/arrow/toolStates/Pointing.mjs +1 -1
  95. package/dist-esm/lib/shapes/arrow/toolStates/Pointing.mjs.map +2 -2
  96. package/dist-esm/lib/shapes/bookmark/BookmarkShapeUtil.mjs +4 -5
  97. package/dist-esm/lib/shapes/bookmark/BookmarkShapeUtil.mjs.map +2 -2
  98. package/dist-esm/lib/shapes/frame/FrameShapeUtil.mjs +2 -1
  99. package/dist-esm/lib/shapes/frame/FrameShapeUtil.mjs.map +2 -2
  100. package/dist-esm/lib/shapes/frame/components/FrameLabelInput.mjs +9 -3
  101. package/dist-esm/lib/shapes/frame/components/FrameLabelInput.mjs.map +2 -2
  102. package/dist-esm/lib/shapes/geo/GeoShapeUtil.mjs +1 -0
  103. package/dist-esm/lib/shapes/geo/GeoShapeUtil.mjs.map +2 -2
  104. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs +2 -1
  105. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs.map +2 -2
  106. package/dist-esm/lib/shapes/shared/HyperlinkButton.mjs +5 -5
  107. package/dist-esm/lib/shapes/shared/HyperlinkButton.mjs.map +2 -2
  108. package/dist-esm/lib/shapes/shared/useEditablePlainText.mjs +3 -4
  109. package/dist-esm/lib/shapes/shared/useEditablePlainText.mjs.map +2 -2
  110. package/dist-esm/lib/shapes/text/PlainTextArea.mjs +4 -3
  111. package/dist-esm/lib/shapes/text/PlainTextArea.mjs.map +2 -2
  112. package/dist-esm/lib/shapes/text/RichTextArea.mjs +3 -4
  113. package/dist-esm/lib/shapes/text/RichTextArea.mjs.map +2 -2
  114. package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs +3 -1
  115. package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs.map +2 -2
  116. package/dist-esm/lib/ui/components/A11y.mjs +1 -2
  117. package/dist-esm/lib/ui/components/A11y.mjs.map +2 -2
  118. package/dist-esm/lib/ui/components/AccessibilityMenu.mjs +3 -3
  119. package/dist-esm/lib/ui/components/AccessibilityMenu.mjs.map +2 -2
  120. package/dist-esm/lib/ui/components/InputModeMenu.mjs +57 -0
  121. package/dist-esm/lib/ui/components/InputModeMenu.mjs.map +7 -0
  122. package/dist-esm/lib/ui/components/LanguageMenu.mjs +1 -0
  123. package/dist-esm/lib/ui/components/LanguageMenu.mjs.map +2 -2
  124. package/dist-esm/lib/ui/components/MainMenu/DefaultMainMenuContent.mjs +2 -0
  125. package/dist-esm/lib/ui/components/MainMenu/DefaultMainMenuContent.mjs.map +2 -2
  126. package/dist-esm/lib/ui/components/Minimap/DefaultMinimap.mjs +2 -1
  127. package/dist-esm/lib/ui/components/Minimap/DefaultMinimap.mjs.map +2 -2
  128. package/dist-esm/lib/ui/components/PageMenu/DefaultPageMenu.mjs +1 -2
  129. package/dist-esm/lib/ui/components/PageMenu/DefaultPageMenu.mjs.map +2 -2
  130. package/dist-esm/lib/ui/components/StylePanel/DefaultStylePanel.mjs +4 -2
  131. package/dist-esm/lib/ui/components/StylePanel/DefaultStylePanel.mjs.map +2 -2
  132. package/dist-esm/lib/ui/components/StylePanel/DefaultStylePanelContent.mjs +2 -2
  133. package/dist-esm/lib/ui/components/StylePanel/DefaultStylePanelContent.mjs.map +2 -2
  134. package/dist-esm/lib/ui/components/StylePanel/StylePanelButtonPicker.mjs +12 -3
  135. package/dist-esm/lib/ui/components/StylePanel/StylePanelButtonPicker.mjs.map +2 -2
  136. package/dist-esm/lib/ui/components/StylePanel/StylePanelContext.mjs +4 -2
  137. package/dist-esm/lib/ui/components/StylePanel/StylePanelContext.mjs.map +2 -2
  138. package/dist-esm/lib/ui/components/Toolbar/AltTextEditor.mjs +3 -2
  139. package/dist-esm/lib/ui/components/Toolbar/AltTextEditor.mjs.map +2 -2
  140. package/dist-esm/lib/ui/components/Toolbar/LinkEditor.mjs +5 -4
  141. package/dist-esm/lib/ui/components/Toolbar/LinkEditor.mjs.map +2 -2
  142. package/dist-esm/lib/ui/components/Toolbar/OverflowingToolbar.mjs +1 -1
  143. package/dist-esm/lib/ui/components/Toolbar/OverflowingToolbar.mjs.map +2 -2
  144. package/dist-esm/lib/ui/components/Toolbar/ToggleToolLockedButton.mjs +6 -2
  145. package/dist-esm/lib/ui/components/Toolbar/ToggleToolLockedButton.mjs.map +2 -2
  146. package/dist-esm/lib/ui/components/menu-items.mjs +6 -4
  147. package/dist-esm/lib/ui/components/menu-items.mjs.map +2 -2
  148. package/dist-esm/lib/ui/components/primitives/TldrawUiContextualToolbar.mjs +1 -2
  149. package/dist-esm/lib/ui/components/primitives/TldrawUiContextualToolbar.mjs.map +2 -2
  150. package/dist-esm/lib/ui/components/primitives/TldrawUiInput.mjs +6 -4
  151. package/dist-esm/lib/ui/components/primitives/TldrawUiInput.mjs.map +2 -2
  152. package/dist-esm/lib/ui/components/primitives/TldrawUiSlider.mjs +1 -1
  153. package/dist-esm/lib/ui/components/primitives/TldrawUiSlider.mjs.map +2 -2
  154. package/dist-esm/lib/ui/components/primitives/TldrawUiToolbar.mjs +1 -0
  155. package/dist-esm/lib/ui/components/primitives/TldrawUiToolbar.mjs.map +2 -2
  156. package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs +69 -20
  157. package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs.map +2 -2
  158. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuCheckboxItem.mjs +3 -0
  159. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuCheckboxItem.mjs.map +2 -2
  160. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs +8 -8
  161. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs.map +2 -2
  162. package/dist-esm/lib/ui/context/actions.mjs +11 -25
  163. package/dist-esm/lib/ui/context/actions.mjs.map +2 -2
  164. package/dist-esm/lib/ui/context/events.mjs.map +2 -2
  165. package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs +1 -2
  166. package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs.map +2 -2
  167. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs +11 -3
  168. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs.map +2 -2
  169. package/dist-esm/lib/ui/version.mjs +3 -3
  170. package/dist-esm/lib/ui/version.mjs.map +1 -1
  171. package/package.json +3 -3
  172. package/src/index.ts +8 -1
  173. package/src/lib/defaultExternalContentHandlers.ts +14 -0
  174. package/src/lib/shapes/arrow/ArrowShapeOptions.test.ts +83 -13
  175. package/src/lib/shapes/arrow/ArrowShapeTool.test.ts +99 -5
  176. package/src/lib/shapes/arrow/ArrowShapeUtil.test.ts +41 -0
  177. package/src/lib/shapes/arrow/arrow-types.ts +3 -5
  178. package/src/lib/shapes/arrow/arrowTargetState.ts +34 -3
  179. package/src/lib/shapes/arrow/toolStates/Pointing.tsx +1 -1
  180. package/src/lib/shapes/bookmark/BookmarkShapeUtil.tsx +4 -5
  181. package/src/lib/shapes/frame/FrameShapeUtil.tsx +1 -0
  182. package/src/lib/shapes/frame/components/FrameLabelInput.tsx +10 -3
  183. package/src/lib/shapes/geo/GeoShapeUtil.tsx +1 -0
  184. package/src/lib/shapes/note/NoteShapeUtil.tsx +1 -0
  185. package/src/lib/shapes/shared/HyperlinkButton.tsx +5 -5
  186. package/src/lib/shapes/shared/useEditablePlainText.ts +3 -4
  187. package/src/lib/shapes/text/PlainTextArea.tsx +4 -3
  188. package/src/lib/shapes/text/RichTextArea.tsx +3 -4
  189. package/src/lib/tools/SelectTool/childStates/DraggingHandle.tsx +6 -2
  190. package/src/lib/ui/components/A11y.tsx +1 -2
  191. package/src/lib/ui/components/AccessibilityMenu.tsx +2 -2
  192. package/src/lib/ui/components/InputModeMenu.tsx +65 -0
  193. package/src/lib/ui/components/LanguageMenu.tsx +1 -0
  194. package/src/lib/ui/components/MainMenu/DefaultMainMenuContent.tsx +4 -0
  195. package/src/lib/ui/components/Minimap/DefaultMinimap.tsx +2 -1
  196. package/src/lib/ui/components/PageMenu/DefaultPageMenu.tsx +1 -2
  197. package/src/lib/ui/components/StylePanel/DefaultStylePanel.tsx +4 -2
  198. package/src/lib/ui/components/StylePanel/DefaultStylePanelContent.tsx +4 -2
  199. package/src/lib/ui/components/StylePanel/StylePanelButtonPicker.tsx +10 -3
  200. package/src/lib/ui/components/StylePanel/StylePanelContext.tsx +5 -3
  201. package/src/lib/ui/components/Toolbar/AltTextEditor.tsx +4 -3
  202. package/src/lib/ui/components/Toolbar/LinkEditor.tsx +6 -5
  203. package/src/lib/ui/components/Toolbar/OverflowingToolbar.tsx +1 -1
  204. package/src/lib/ui/components/Toolbar/ToggleToolLockedButton.tsx +9 -2
  205. package/src/lib/ui/components/menu-items.tsx +5 -3
  206. package/src/lib/ui/components/primitives/TldrawUiContextualToolbar.tsx +1 -2
  207. package/src/lib/ui/components/primitives/TldrawUiInput.tsx +6 -3
  208. package/src/lib/ui/components/primitives/TldrawUiSlider.tsx +2 -2
  209. package/src/lib/ui/components/primitives/TldrawUiToolbar.tsx +2 -1
  210. package/src/lib/ui/components/primitives/TldrawUiTooltip.tsx +65 -14
  211. package/src/lib/ui/components/primitives/menus/TldrawUiMenuCheckboxItem.tsx +4 -0
  212. package/src/lib/ui/components/primitives/menus/TldrawUiMenuItem.tsx +9 -9
  213. package/src/lib/ui/context/actions.tsx +18 -27
  214. package/src/lib/ui/context/events.tsx +2 -1
  215. package/src/lib/ui/hooks/useClipboardEvents.ts +1 -2
  216. package/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts +10 -2
  217. package/src/lib/ui/hooks/useTranslation/defaultTranslation.ts +11 -3
  218. package/src/lib/ui/version.ts +3 -3
  219. package/src/lib/ui.css +25 -2
  220. package/src/test/TestEditor.ts +8 -2
  221. package/src/test/commands/setCamera.test.ts +13 -0
  222. package/src/test/frames.test.ts +15 -0
  223. package/src/test/getCulledShapes.test.tsx +71 -2
  224. package/tldraw.css +33 -5
@@ -6,6 +6,7 @@ import React, {
6
6
  ReactNode,
7
7
  useContext,
8
8
  useEffect,
9
+ useLayoutEffect,
9
10
  useRef,
10
11
  useState,
11
12
  } from 'react'
@@ -24,18 +25,20 @@ export interface TldrawUiTooltipProps {
24
25
  delayDuration?: number
25
26
  }
26
27
 
28
+ interface CurrentTooltip {
29
+ id: string
30
+ content: ReactNode
31
+ side: 'top' | 'right' | 'bottom' | 'left'
32
+ sideOffset: number
33
+ showOnMobile: boolean
34
+ targetElement: HTMLElement
35
+ delayDuration: number
36
+ }
37
+
27
38
  // Singleton tooltip manager
28
39
  class TooltipManager {
29
40
  private static instance: TooltipManager | null = null
30
- private currentTooltip = atom<{
31
- id: string
32
- content: ReactNode
33
- side: 'top' | 'right' | 'bottom' | 'left'
34
- sideOffset: number
35
- showOnMobile: boolean
36
- targetElement: HTMLElement
37
- delayDuration: number
38
- } | null>('current tooltip', null)
41
+ private currentTooltip = atom<CurrentTooltip | null>('current tooltip', null)
39
42
  private destroyTimeoutId: number | null = null
40
43
 
41
44
  static getInstance(): TooltipManager {
@@ -72,6 +75,15 @@ class TooltipManager {
72
75
  })
73
76
  }
74
77
 
78
+ updateCurrentTooltip(tooltipId: string, update: (tooltip: CurrentTooltip) => CurrentTooltip) {
79
+ this.currentTooltip.update((tooltip) => {
80
+ if (tooltip?.id === tooltipId) {
81
+ return update(tooltip)
82
+ }
83
+ return tooltip
84
+ })
85
+ }
86
+
75
87
  hideTooltip(editor: Editor | null, tooltipId: string, instant: boolean = false) {
76
88
  const hide = () => {
77
89
  // Only hide if this is the current tooltip
@@ -159,6 +171,20 @@ function TooltipSingleton() {
159
171
  }
160
172
  }, [cameraState, isOpen, currentTooltip, editor])
161
173
 
174
+ useEffect(() => {
175
+ function handleKeyDown(event: KeyboardEvent) {
176
+ if (event.key === 'Escape' && currentTooltip && isOpen) {
177
+ tooltipManager.hideTooltip(editor, currentTooltip.id)
178
+ event.stopPropagation()
179
+ }
180
+ }
181
+
182
+ document.addEventListener('keydown', handleKeyDown, { capture: true })
183
+ return () => {
184
+ document.removeEventListener('keydown', handleKeyDown, { capture: true })
185
+ }
186
+ }, [editor, currentTooltip, isOpen])
187
+
162
188
  // Update open state and trigger position
163
189
  useEffect(() => {
164
190
  let timer: ReturnType<typeof setTimeout> | null = null
@@ -241,6 +267,11 @@ export const TldrawUiTooltip = forwardRef<HTMLButtonElement, TldrawUiTooltipProp
241
267
  const editor = useMaybeEditor()
242
268
  const tooltipId = useRef<string>(uniqueId())
243
269
  const hasProvider = useContext(TooltipSingletonContext)
270
+ const enhancedA11yMode = useValue(
271
+ 'enhancedA11yMode',
272
+ () => editor?.user.getEnhancedA11yMode(),
273
+ [editor]
274
+ )
244
275
 
245
276
  const orientationCtx = useTldrawUiOrientation()
246
277
  const sideToUse = side ?? orientationCtx.tooltipSide
@@ -254,18 +285,38 @@ export const TldrawUiTooltip = forwardRef<HTMLButtonElement, TldrawUiTooltipProp
254
285
  }
255
286
  }, [editor, hasProvider])
256
287
 
257
- // Don't show tooltip if disabled, no content, or UI labels are disabled
288
+ useLayoutEffect(() => {
289
+ if (hasProvider && tooltipManager.getCurrentTooltipData()?.id === tooltipId.current) {
290
+ tooltipManager.updateCurrentTooltip(tooltipId.current, (tooltip) => ({
291
+ ...tooltip,
292
+ content,
293
+ side: sideToUse,
294
+ sideOffset,
295
+ showOnMobile,
296
+ }))
297
+ }
298
+ }, [content, sideToUse, sideOffset, showOnMobile, hasProvider])
299
+
300
+ // Don't show tooltip if disabled, no content, or enhanced accessibility mode is disabled
258
301
  if (disabled || !content) {
259
302
  return <>{children}</>
260
303
  }
261
304
 
262
- const delayDurationToUse =
263
- delayDuration ?? (editor?.options.tooltipDelayMs || DEFAULT_TOOLTIP_DELAY_MS)
305
+ let delayDurationToUse
306
+ if (enhancedA11yMode) {
307
+ delayDurationToUse = 0
308
+ } else {
309
+ delayDurationToUse =
310
+ delayDuration ?? (editor?.options.tooltipDelayMs || DEFAULT_TOOLTIP_DELAY_MS)
311
+ }
264
312
 
265
313
  // Fallback to old behavior if no provider
266
- if (!hasProvider) {
314
+ if (!hasProvider || enhancedA11yMode) {
267
315
  return (
268
- <_Tooltip.Root delayDuration={delayDurationToUse} disableHoverableContent>
316
+ <_Tooltip.Root
317
+ delayDuration={delayDurationToUse}
318
+ disableHoverableContent={!enhancedA11yMode}
319
+ >
269
320
  <_Tooltip.Trigger asChild ref={ref}>
270
321
  {children}
271
322
  </_Tooltip.Trigger>
@@ -19,6 +19,7 @@ export interface TLUiMenuCheckboxItemProps<
19
19
  kbd?: string
20
20
  title?: string
21
21
  label?: TranslationKey | { [key: string]: TranslationKey }
22
+ lang?: string
22
23
  readonlyOk?: boolean
23
24
  onSelect(source: TLUiEventSource): Promise<void> | void
24
25
  toggle?: boolean
@@ -34,6 +35,7 @@ export function TldrawUiMenuCheckboxItem<
34
35
  id,
35
36
  kbd,
36
37
  label,
38
+ lang,
37
39
  readonlyOk,
38
40
  onSelect,
39
41
  toggle = false,
@@ -55,6 +57,7 @@ export function TldrawUiMenuCheckboxItem<
55
57
  return (
56
58
  <_DropdownMenu.CheckboxItem
57
59
  dir="ltr"
60
+ lang={lang}
58
61
  className="tlui-button tlui-button__menu tlui-button__checkbox"
59
62
  title={labelStr}
60
63
  onSelect={(e) => {
@@ -84,6 +87,7 @@ export function TldrawUiMenuCheckboxItem<
84
87
  key={id}
85
88
  className="tlui-button tlui-button__menu tlui-button__checkbox"
86
89
  dir="ltr"
90
+ lang={lang}
87
91
  title={labelStr}
88
92
  onSelect={(e) => {
89
93
  onSelect(sourceId)
@@ -213,7 +213,7 @@ export function TldrawUiMenuItem<
213
213
  icon={icon}
214
214
  onSelect={onSelect}
215
215
  onDragStart={onDragStart}
216
- labelToUse={labelToUse}
216
+ labelStr={labelStr}
217
217
  titleStr={titleStr}
218
218
  disabled={disabled}
219
219
  isSelected={isSelected}
@@ -247,7 +247,7 @@ export function TldrawUiMenuItem<
247
247
  icon={icon}
248
248
  onSelect={onSelect}
249
249
  onDragStart={onDragStart}
250
- labelToUse={labelToUse}
250
+ labelStr={labelStr}
251
251
  titleStr={titleStr}
252
252
  disabled={disabled}
253
253
  isSelected={isSelected}
@@ -333,7 +333,7 @@ function useDraggableEvents(
333
333
  type: 'pointer',
334
334
  target: 'canvas',
335
335
  name: 'pointer_down',
336
- ...getPointerInfo(e),
336
+ ...getPointerInfo(editor, e),
337
337
  point: screenSpaceStart,
338
338
  })
339
339
 
@@ -345,7 +345,7 @@ function useDraggableEvents(
345
345
  type: 'pointer',
346
346
  target: 'canvas',
347
347
  name: 'pointer_move',
348
- ...getPointerInfo(e),
348
+ ...getPointerInfo(editor, e),
349
349
  point: screenSpaceStart,
350
350
  })
351
351
 
@@ -365,7 +365,7 @@ function useDraggableEvents(
365
365
  type: 'pointer',
366
366
  target: 'canvas',
367
367
  name: 'pointer_up',
368
- ...getPointerInfo(e),
368
+ ...getPointerInfo(editor, e),
369
369
  })
370
370
  }
371
371
 
@@ -392,7 +392,7 @@ function useDraggableEvents(
392
392
 
393
393
  function DraggableToolbarButton({
394
394
  id,
395
- labelToUse,
395
+ labelStr,
396
396
  titleStr,
397
397
  disabled,
398
398
  isSelected,
@@ -403,7 +403,7 @@ function DraggableToolbarButton({
403
403
  }: {
404
404
  id: string
405
405
  disabled: boolean
406
- labelToUse?: string
406
+ labelStr?: string
407
407
  titleStr?: string
408
408
  isSelected?: boolean
409
409
  icon: TLUiMenuItemProps['icon']
@@ -416,7 +416,7 @@ function DraggableToolbarButton({
416
416
  if (overflow) {
417
417
  return (
418
418
  <TldrawUiToolbarButton
419
- aria-label={labelToUse}
419
+ aria-label={labelStr}
420
420
  aria-pressed={isSelected ? 'true' : 'false'}
421
421
  isActive={isSelected}
422
422
  className="tlui-button-grid__button"
@@ -434,7 +434,7 @@ function DraggableToolbarButton({
434
434
 
435
435
  return (
436
436
  <TldrawUiToolbarButton
437
- aria-label={labelToUse}
437
+ aria-label={labelStr}
438
438
  aria-pressed={isSelected ? 'true' : 'false'}
439
439
  data-testid={`tools.${id}`}
440
440
  data-value={id}
@@ -1270,16 +1270,16 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
1270
1270
  checkbox: true,
1271
1271
  },
1272
1272
  {
1273
- id: 'toggle-ui-labels',
1273
+ id: 'enhanced-a11y-mode',
1274
1274
  label: {
1275
- default: 'action.toggle-ui-labels',
1276
- menu: 'action.toggle-ui-labels.menu',
1275
+ default: 'action.enhanced-a11y-mode',
1276
+ menu: 'action.enhanced-a11y-mode.menu',
1277
1277
  },
1278
1278
  readonlyOk: true,
1279
1279
  onSelect(source) {
1280
- trackEvent('toggle-ui-labels', { source })
1280
+ trackEvent('enhanced-a11y-mode', { source })
1281
1281
  editor.user.updateUserPreferences({
1282
- showUiLabels: !editor.user.getShowUiLabels(),
1282
+ enhancedA11yMode: !editor.user.getEnhancedA11yMode(),
1283
1283
  })
1284
1284
  },
1285
1285
  checkbox: true,
@@ -1344,28 +1344,6 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
1344
1344
  }
1345
1345
  },
1346
1346
  },
1347
- {
1348
- id: 'toggle-focus-mode',
1349
- label: {
1350
- default: 'action.toggle-focus-mode',
1351
- menu: 'action.toggle-focus-mode.menu',
1352
- },
1353
- readonlyOk: true,
1354
- kbd: 'cmd+.,ctrl+.',
1355
- checkbox: true,
1356
- onSelect(source) {
1357
- // this needs to be deferred because it causes the menu
1358
- // UI to unmount which puts us in a dodgy state
1359
- editor.timers.requestAnimationFrame(() => {
1360
- editor.run(() => {
1361
- trackEvent('toggle-focus-mode', { source })
1362
- helpers.clearDialogs()
1363
- helpers.clearToasts()
1364
- editor.updateInstanceState({ isFocusMode: !editor.getInstanceState().isFocusMode })
1365
- })
1366
- })
1367
- },
1368
- },
1369
1347
  {
1370
1348
  id: 'toggle-grid',
1371
1349
  label: {
@@ -1584,6 +1562,19 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
1584
1562
  onSelect: async (source) => {
1585
1563
  if (!canApplySelectionAction()) return
1586
1564
 
1565
+ const onlySelectedShape = editor.getOnlySelectedShape()
1566
+ if (
1567
+ onlySelectedShape &&
1568
+ (editor.isShapeOfType<TLImageShape>(onlySelectedShape, 'image') ||
1569
+ editor.isShapeOfType<TLVideoShape>(onlySelectedShape, 'video'))
1570
+ ) {
1571
+ const firstToolbarButton = editor
1572
+ .getContainer()
1573
+ .querySelector('.tlui-contextual-toolbar button:first-child') as HTMLElement | null
1574
+ firstToolbarButton?.focus()
1575
+ return
1576
+ }
1577
+
1587
1578
  const firstButton = editor
1588
1579
  .getContainer()
1589
1580
  .querySelector('.tlui-style-panel button') as HTMLElement | null
@@ -102,13 +102,14 @@ export interface TLUiEventMap {
102
102
  'toggle-grid-mode': null
103
103
  'toggle-wrap-mode': null
104
104
  'toggle-focus-mode': null
105
+ 'input-mode': { value: string }
105
106
  'toggle-debug-mode': null
106
107
  'toggle-dynamic-size-mode': null
107
108
  'toggle-paste-at-cursor': null
108
109
  'toggle-lock': null
109
110
  'toggle-reduce-motion': null
110
111
  'toggle-keyboard-shortcuts': null
111
- 'toggle-ui-labels': null
112
+ 'enhanced-a11y-mode': null
112
113
  'toggle-edge-scrolling': null
113
114
  'color-scheme': { value: string }
114
115
  'exit-pen-mode': null
@@ -8,7 +8,6 @@ import {
8
8
  compact,
9
9
  isDefined,
10
10
  preventDefault,
11
- stopEventPropagation,
12
11
  uniq,
13
12
  useEditor,
14
13
  useMaybeEditor,
@@ -763,7 +762,7 @@ export function useNativeClipboardEvents() {
763
762
 
764
763
  const paste = (e: ClipboardEvent) => {
765
764
  if (disablingMiddleClickPaste) {
766
- stopEventPropagation(e)
765
+ editor.markEventAsHandled(e)
767
766
  return
768
767
  }
769
768
 
@@ -3,6 +3,11 @@
3
3
 
4
4
  /** @public */
5
5
  export type TLUiTranslationKey =
6
+ | 'action.toggle-auto-pan'
7
+ | 'action.toggle-auto-zoom'
8
+ | 'action.toggle-auto-none'
9
+ | 'action.toggle-mouse'
10
+ | 'action.toggle-trackpad'
6
11
  | 'action.convert-to-bookmark'
7
12
  | 'action.convert-to-embed'
8
13
  | 'action.open-embed-link'
@@ -93,8 +98,8 @@ export type TLUiTranslationKey =
93
98
  | 'action.toggle-reduce-motion'
94
99
  | 'action.toggle-keyboard-shortcuts.menu'
95
100
  | 'action.toggle-keyboard-shortcuts'
96
- | 'action.toggle-ui-labels.menu'
97
- | 'action.toggle-ui-labels'
101
+ | 'action.enhanced-a11y-mode.menu'
102
+ | 'action.enhanced-a11y-mode'
98
103
  | 'action.toggle-edge-scrolling.menu'
99
104
  | 'action.toggle-edge-scrolling'
100
105
  | 'action.toggle-debug-mode.menu'
@@ -122,6 +127,7 @@ export type TLUiTranslationKey =
122
127
  | 'action.zoom-to-fit'
123
128
  | 'action.zoom-to-selection'
124
129
  | 'assets.files.size-too-big'
130
+ | 'assets.files.maximum-size'
125
131
  | 'assets.files.type-not-allowed'
126
132
  | 'assets.files.upload-failed'
127
133
  | 'assets.files.amount-too-many'
@@ -310,6 +316,7 @@ export type TLUiTranslationKey =
310
316
  | 'menu.language'
311
317
  | 'menu.preferences'
312
318
  | 'menu.view'
319
+ | 'menu.input-mode'
313
320
  | 'context-menu.title'
314
321
  | 'context-menu.edit'
315
322
  | 'context-menu.arrange'
@@ -411,6 +418,7 @@ export type TLUiTranslationKey =
411
418
  | 'style-panel.opacity'
412
419
  | 'style-panel.size'
413
420
  | 'style-panel.spline'
421
+ | 'style-panel.selected'
414
422
  | 'tool-panel.title'
415
423
  | 'tool-panel.more'
416
424
  | 'navigation-zone.title'
@@ -3,6 +3,11 @@
3
3
 
4
4
  /** @internal */
5
5
  export const DEFAULT_TRANSLATION = {
6
+ 'action.toggle-auto-pan': 'Auto (trackpad)',
7
+ 'action.toggle-auto-zoom': 'Auto (mouse)',
8
+ 'action.toggle-auto-none': 'Auto',
9
+ 'action.toggle-mouse': 'Mouse',
10
+ 'action.toggle-trackpad': 'Trackpad',
6
11
  'action.convert-to-bookmark': 'Convert to Bookmark',
7
12
  'action.convert-to-embed': 'Convert to Embed',
8
13
  'action.open-embed-link': 'Open link',
@@ -92,10 +97,10 @@ export const DEFAULT_TRANSLATION = {
92
97
  'action.toggle-wrap-mode': 'Toggle Select on wrap',
93
98
  'action.toggle-reduce-motion.menu': 'Reduce motion',
94
99
  'action.toggle-reduce-motion': 'Toggle reduce motion',
95
- 'action.toggle-keyboard-shortcuts.menu': 'Keyboard shortcuts',
100
+ 'action.toggle-keyboard-shortcuts.menu': 'Enable keyboard shortcuts',
96
101
  'action.toggle-keyboard-shortcuts': 'Toggle keyboard shortcuts',
97
- 'action.toggle-ui-labels.menu': 'UI labels',
98
- 'action.toggle-ui-labels': 'Toggle UI labels',
102
+ 'action.enhanced-a11y-mode.menu': 'Enhanced accessibility mode',
103
+ 'action.enhanced-a11y-mode': 'Toggle enhanced accessibility mode',
99
104
  'action.toggle-edge-scrolling.menu': 'Edge scrolling',
100
105
  'action.toggle-edge-scrolling': 'Toggle edge scrolling',
101
106
  'action.toggle-debug-mode.menu': 'Debug mode',
@@ -123,6 +128,7 @@ export const DEFAULT_TRANSLATION = {
123
128
  'action.zoom-to-fit': 'Zoom to fit',
124
129
  'action.zoom-to-selection': 'Zoom to selection',
125
130
  'assets.files.size-too-big': 'File size is too big',
131
+ 'assets.files.maximum-size': 'Maximum file size is {size}',
126
132
  'assets.files.type-not-allowed': 'File type is not allowed',
127
133
  'assets.files.upload-failed': 'Upload failed',
128
134
  'assets.files.amount-too-many': 'Too many files',
@@ -311,6 +317,7 @@ export const DEFAULT_TRANSLATION = {
311
317
  'menu.language': 'Language',
312
318
  'menu.preferences': 'Preferences',
313
319
  'menu.view': 'View',
320
+ 'menu.input-mode': 'Input mode',
314
321
  'context-menu.title': 'Context menu',
315
322
  'context-menu.edit': 'Edit',
316
323
  'context-menu.arrange': 'Arrange',
@@ -414,6 +421,7 @@ export const DEFAULT_TRANSLATION = {
414
421
  'style-panel.opacity': 'Opacity',
415
422
  'style-panel.size': 'Size',
416
423
  'style-panel.spline': 'Spline',
424
+ 'style-panel.selected': 'selected',
417
425
  'tool-panel.title': 'Tools',
418
426
  'tool-panel.more': 'More',
419
427
  'navigation-zone.title': 'Navigation',
@@ -1,9 +1,9 @@
1
1
  // This file is automatically generated by internal/scripts/refresh-assets.ts.
2
2
  // Do not edit manually. Or do, I'm a comment, not a cop.
3
3
 
4
- export const version = '3.16.0-next.4337ae1ab96d'
4
+ export const version = '3.16.0-next.8eb6d5c2d8f4'
5
5
  export const publishDates = {
6
6
  major: '2024-09-13T14:36:29.063Z',
7
- minor: '2025-09-03T14:48:08.249Z',
8
- patch: '2025-09-03T14:48:08.249Z',
7
+ minor: '2025-09-18T14:19:10.006Z',
8
+ patch: '2025-09-18T14:19:10.006Z',
9
9
  }
package/src/lib/ui.css CHANGED
@@ -168,7 +168,7 @@
168
168
  min-height: 40px;
169
169
  width: 100%;
170
170
  gap: 8px;
171
- margin: -4px 0px;
171
+ margin-top: -4px;
172
172
  }
173
173
 
174
174
  .tlui-button__menu::after {
@@ -494,6 +494,10 @@
494
494
  -webkit-user-select: auto !important;
495
495
  }
496
496
 
497
+ .tlui-input::placeholder {
498
+ color: var(--tl-color-text-3);
499
+ }
500
+
497
501
  .tlui-input__wrapper {
498
502
  width: 100%;
499
503
  height: 44px;
@@ -578,6 +582,12 @@
578
582
  box-shadow: var(--tl-shadow-3);
579
583
  }
580
584
 
585
+ @media (max-height: 600px) {
586
+ .tlui-menu {
587
+ max-height: 70vh;
588
+ }
589
+ }
590
+
581
591
  .tlui-menu::-webkit-scrollbar {
582
592
  display: none;
583
593
  }
@@ -987,7 +997,7 @@
987
997
  max-width: 148px;
988
998
  }
989
999
 
990
- .tlui-style-panel[data-show-ui-labels='true'] .tlui-button[data-isactive='true'] {
1000
+ .tlui-style-panel[data-enhanced-a11y-mode='true'] .tlui-button[data-isactive='true'] {
991
1001
  border-radius: 10px;
992
1002
  outline: 2px solid var(--tl-color-text);
993
1003
  outline-offset: -5px;
@@ -1029,6 +1039,19 @@ tldraw? probably.
1029
1039
  display: none;
1030
1040
  }
1031
1041
 
1042
+ /*
1043
+ * This is used in a couple places, like Align and Vertical Align.
1044
+ * It's because we have a toolbar with a Toggle Group but then an adjacent button
1045
+ * next to it that opens a popup.
1046
+ */
1047
+ .tlui-style-panel__section .tlui-toolbar:has(.tlui-toolbar) {
1048
+ flex-wrap: wrap;
1049
+ }
1050
+
1051
+ .tlui-style-panel__section .tlui-toolbar:has(.tlui-toolbar) .tlui-style-panel__subheading {
1052
+ margin-left: -2px;
1053
+ }
1054
+
1032
1055
  .tlui-style-panel__section__common:not(:only-child) {
1033
1056
  margin-bottom: 7px;
1034
1057
  border-bottom: 1px solid var(--tl-color-divider);
@@ -86,8 +86,14 @@ export class TestEditor extends Editor {
86
86
  elm.tabIndex = 0
87
87
  elm.getBoundingClientRect = () => bounds as DOMRect
88
88
 
89
- const shapeUtilsWithDefaults = [...defaultShapeUtils, ...(options.shapeUtils ?? [])]
90
- const bindingUtilsWithDefaults = [...defaultBindingUtils, ...(options.bindingUtils ?? [])]
89
+ const shapeUtilsWithDefaults = [
90
+ ...defaultShapeUtils.filter((s) => !options.shapeUtils?.some((su) => su.type === s.type)),
91
+ ...(options.shapeUtils ?? []),
92
+ ]
93
+ const bindingUtilsWithDefaults = [
94
+ ...defaultBindingUtils.filter((b) => !options.bindingUtils?.some((bu) => bu.type === b.type)),
95
+ ...(options.bindingUtils ?? []),
96
+ ]
91
97
 
92
98
  super({
93
99
  ...options,
@@ -12,6 +12,7 @@ beforeEach(() => {
12
12
  },
13
13
  })
14
14
  editor.updateViewportScreenBounds(new Box(0, 0, 1600, 900))
15
+ editor.user.updateUserPreferences({ inputMode: null })
15
16
  })
16
17
 
17
18
  const wheelEvent = {
@@ -204,6 +205,18 @@ describe('CameraOptions.wheelBehavior', () => {
204
205
  .forceTick()
205
206
  expect(editor.getCamera()).toMatchObject({ x: 0, y: 5, z: 1 })
206
207
  })
208
+
209
+ it('When input mode is set, camera wheel behavior is ignored', () => {
210
+ editor.user.updateUserPreferences({ inputMode: 'trackpad' })
211
+ editor
212
+ .setCameraOptions({ ...DEFAULT_CAMERA_OPTIONS, wheelBehavior: 'zoom' })
213
+ .dispatch({
214
+ ...wheelEvent,
215
+ delta: new Vec(0, 5, 0.01),
216
+ })
217
+ .forceTick()
218
+ expect(editor.getCamera()).toMatchObject({ x: 0, y: 5, z: 1 })
219
+ })
207
220
  })
208
221
 
209
222
  describe('CameraOptions.panSpeed', () => {
@@ -1680,3 +1680,18 @@ it('drops into the top-most frame, if there is one', () => {
1680
1680
 
1681
1681
  expect(editor.getShape(rect)?.parentId).toBe(editor.getCurrentPageId())
1682
1682
  })
1683
+
1684
+ it('does not get drop children of nested frame if they are occluded from the outer frame', () => {
1685
+ const frame1Id = dragCreateFrame({ down: [100, 100], move: [300, 300], up: [300, 300] })
1686
+ const frame2Id = dragCreateFrame({ down: [150, 150], move: [290, 290], up: [290, 290] })
1687
+
1688
+ const rect1 = createRect({ pos: [280, 160], size: [10, 30] })
1689
+
1690
+ expect(editor.getShape(rect1)?.parentId).toBe(frame2Id)
1691
+ expect(editor.getShape(frame2Id)?.parentId).toBe(frame1Id)
1692
+
1693
+ editor.select(frame2Id)
1694
+ editor.translateSelection(30, 0)
1695
+
1696
+ expect(editor.getShape(rect1)?.parentId).toBe(frame2Id)
1697
+ })
@@ -1,12 +1,50 @@
1
- import { Box, TLShapeId, createShapeId } from '@tldraw/editor'
1
+ import {
2
+ BaseBoxShapeUtil,
3
+ Box,
4
+ RecordProps,
5
+ T,
6
+ TLBaseShape,
7
+ TLShapeId,
8
+ createShapeId,
9
+ } from '@tldraw/editor'
2
10
  import { vi } from 'vitest'
3
11
  import { TestEditor } from './TestEditor'
4
12
  import { TL } from './test-jsx'
5
13
 
14
+ // Custom uncullable shape type for testing canCull override
15
+ type UncullableShape = TLBaseShape<'uncullable', { w: number; h: number }>
16
+
17
+ class UncullableShapeUtil extends BaseBoxShapeUtil<UncullableShape> {
18
+ static override type = 'uncullable' as const
19
+ static override props: RecordProps<UncullableShape> = {
20
+ w: T.number,
21
+ h: T.number,
22
+ }
23
+
24
+ override canCull() {
25
+ return false
26
+ }
27
+
28
+ override getDefaultProps(): UncullableShape['props'] {
29
+ return {
30
+ w: 100,
31
+ h: 100,
32
+ }
33
+ }
34
+
35
+ override component() {
36
+ return <div>Uncullable shape</div>
37
+ }
38
+
39
+ override indicator() {
40
+ return <div>Uncullable shape</div>
41
+ }
42
+ }
43
+
6
44
  let editor: TestEditor
7
45
 
8
46
  beforeEach(() => {
9
- editor = new TestEditor()
47
+ editor = new TestEditor({ shapeUtils: [UncullableShapeUtil] })
10
48
  editor.setScreenBounds({ x: 0, y: 0, w: 1800, h: 900 })
11
49
  })
12
50
 
@@ -203,3 +241,34 @@ it('works for shapes that are outside of the viewport, but are then moved inside
203
241
  // Arrow should also not be culled
204
242
  expect(editor.getCulledShapes()).toEqual(new Set())
205
243
  })
244
+
245
+ it('respects canCull override - shapes that cannot be culled are never culled', () => {
246
+ const cullableShapeId = createShapeId()
247
+ const uncullableShapeId = createShapeId()
248
+
249
+ // Create both shapes outside the viewport
250
+ editor.createShapes([
251
+ {
252
+ id: cullableShapeId,
253
+ type: 'geo',
254
+ x: -2000, // Way outside viewport
255
+ y: -2000,
256
+ props: { w: 100, h: 100 },
257
+ },
258
+ {
259
+ id: uncullableShapeId,
260
+ type: 'uncullable',
261
+ x: -2000, // Way outside viewport
262
+ y: -2000,
263
+ props: { w: 100, h: 100 },
264
+ },
265
+ ])
266
+
267
+ const culledShapes = editor.getCulledShapes()
268
+
269
+ // The regular geo shape should be culled since it's outside the viewport
270
+ expect(culledShapes).toContain(cullableShapeId)
271
+
272
+ // The uncullable shape should NOT be culled even though it's outside the viewport
273
+ expect(culledShapes).not.toContain(uncullableShapeId)
274
+ })