tldraw 4.2.0-canary.08ca3971cdee → 4.2.0-canary.0ebe44a13b81

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 (29) hide show
  1. package/dist-cjs/index.js +1 -1
  2. package/dist-cjs/lib/shapes/frame/components/FrameLabelInput.js +63 -36
  3. package/dist-cjs/lib/shapes/frame/components/FrameLabelInput.js.map +2 -2
  4. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js +3 -3
  5. package/dist-cjs/lib/shapes/note/NoteShapeUtil.js.map +2 -2
  6. package/dist-cjs/lib/ui/components/PageMenu/DefaultPageMenu.js +5 -4
  7. package/dist-cjs/lib/ui/components/PageMenu/DefaultPageMenu.js.map +2 -2
  8. package/dist-cjs/lib/ui/hooks/useTranslation/useTranslation.js +1 -0
  9. package/dist-cjs/lib/ui/hooks/useTranslation/useTranslation.js.map +2 -2
  10. package/dist-cjs/lib/ui/version.js +3 -3
  11. package/dist-cjs/lib/ui/version.js.map +1 -1
  12. package/dist-esm/index.mjs +1 -1
  13. package/dist-esm/lib/shapes/frame/components/FrameLabelInput.mjs +65 -38
  14. package/dist-esm/lib/shapes/frame/components/FrameLabelInput.mjs.map +2 -2
  15. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs +5 -5
  16. package/dist-esm/lib/shapes/note/NoteShapeUtil.mjs.map +2 -2
  17. package/dist-esm/lib/ui/components/PageMenu/DefaultPageMenu.mjs +5 -5
  18. package/dist-esm/lib/ui/components/PageMenu/DefaultPageMenu.mjs.map +2 -2
  19. package/dist-esm/lib/ui/hooks/useTranslation/useTranslation.mjs +1 -0
  20. package/dist-esm/lib/ui/hooks/useTranslation/useTranslation.mjs.map +2 -2
  21. package/dist-esm/lib/ui/version.mjs +3 -3
  22. package/dist-esm/lib/ui/version.mjs.map +1 -1
  23. package/package.json +3 -3
  24. package/src/lib/shapes/frame/components/FrameLabelInput.tsx +48 -24
  25. package/src/lib/shapes/note/NoteShapeUtil.tsx +6 -5
  26. package/src/lib/ui/components/PageMenu/DefaultPageMenu.tsx +6 -5
  27. package/src/lib/ui/hooks/useTranslation/useTranslation.tsx +2 -1
  28. package/src/lib/ui/version.ts +3 -3
  29. package/src/test/TldrawEditor.test.tsx +74 -29
@@ -1,5 +1,8 @@
1
- import { TLFrameShape, TLShapeId, useEditor } from '@tldraw/editor'
2
- import { forwardRef, useCallback } from 'react'
1
+ import { TLFrameShape, TLShapeId, useEditor, useValue } from '@tldraw/editor'
2
+ import { forwardRef, useCallback, useEffect, useRef } from 'react'
3
+ import { PORTRAIT_BREAKPOINT } from '../../../ui/constants'
4
+ import { useBreakpoint } from '../../../ui/context/breakpoints'
5
+ import { useTranslation } from '../../../ui/hooks/useTranslation/useTranslation'
3
6
  import { defaultEmptyAs } from '../FrameShapeUtil'
4
7
 
5
8
  export const FrameLabelInput = forwardRef<
@@ -7,6 +10,15 @@ export const FrameLabelInput = forwardRef<
7
10
  { id: TLShapeId; name: string; isEditing: boolean }
8
11
  >(({ id, name, isEditing }, ref) => {
9
12
  const editor = useEditor()
13
+ const breakpoint = useBreakpoint()
14
+ const isCoarsePointer = useValue(
15
+ 'isCoarsePointer',
16
+ () => editor.getInstanceState().isCoarsePointer,
17
+ [editor]
18
+ )
19
+ const shouldUseWindowPrompt = breakpoint < PORTRAIT_BREAKPOINT.TABLET_SM && isCoarsePointer
20
+ const promptOpen = useRef<boolean>(false)
21
+ const msg = useTranslation()
10
22
 
11
23
  const handlePointerDown = useCallback(
12
24
  (e: React.PointerEvent) => {
@@ -28,13 +40,12 @@ export const FrameLabelInput = forwardRef<
28
40
  [editor]
29
41
  )
30
42
 
31
- const handleBlur = useCallback(
32
- (e: React.FocusEvent<HTMLInputElement>) => {
43
+ const renameFrame = useCallback(
44
+ (value: string) => {
33
45
  const shape = editor.getShape<TLFrameShape>(id)
34
46
  if (!shape) return
35
47
 
36
48
  const name = shape.props.name
37
- const value = e.currentTarget.value.trim()
38
49
  if (name === value) return
39
50
 
40
51
  editor.updateShapes([
@@ -48,36 +59,49 @@ export const FrameLabelInput = forwardRef<
48
59
  [id, editor]
49
60
  )
50
61
 
62
+ const handleBlur = useCallback(
63
+ (e: React.FocusEvent<HTMLInputElement>) => {
64
+ renameFrame(e.currentTarget.value)
65
+ },
66
+ [renameFrame]
67
+ )
68
+
51
69
  const handleChange = useCallback(
52
70
  (e: React.ChangeEvent<HTMLInputElement>) => {
53
- const shape = editor.getShape<TLFrameShape>(id)
54
- if (!shape) return
55
-
56
- const name = shape.props.name
57
- const value = e.currentTarget.value
58
- if (name === value) return
59
-
60
- editor.updateShapes([
61
- {
62
- id,
63
- type: 'frame',
64
- props: { name: value },
65
- },
66
- ])
71
+ renameFrame(e.currentTarget.value)
67
72
  },
68
- [id, editor]
73
+ [renameFrame]
69
74
  )
70
75
 
76
+ /* Mobile rename uses window.prompt */
77
+ useEffect(() => {
78
+ if (!isEditing) {
79
+ promptOpen.current = false
80
+ return
81
+ }
82
+ if (isEditing && shouldUseWindowPrompt && !promptOpen.current) {
83
+ promptOpen.current = true
84
+ const shape = editor.getShape<TLFrameShape>(id)
85
+ const currentName = shape?.props.name ?? ''
86
+ const newName = window.prompt(msg('action.rename'), currentName)
87
+ promptOpen.current = false
88
+ if (newName !== null) renameFrame(newName)
89
+ editor.setEditingShape(null)
90
+ }
91
+ }, [isEditing, shouldUseWindowPrompt, id, msg, renameFrame, editor])
92
+
71
93
  return (
72
- <div className={`tl-frame-label ${isEditing ? 'tl-frame-label__editing' : ''}`}>
94
+ <div
95
+ className={`tl-frame-label ${isEditing && !shouldUseWindowPrompt ? 'tl-frame-label__editing' : ''}`}
96
+ >
73
97
  <input
74
98
  className="tl-frame-name-input"
75
99
  ref={ref}
76
- disabled={!isEditing}
77
- readOnly={!isEditing}
100
+ disabled={!isEditing || shouldUseWindowPrompt}
101
+ readOnly={!isEditing || shouldUseWindowPrompt}
78
102
  style={{ display: isEditing ? undefined : 'none' }}
79
103
  value={name}
80
- autoFocus
104
+ autoFocus={!shouldUseWindowPrompt}
81
105
  onKeyDown={handleKeyDown}
82
106
  onBlur={handleBlur}
83
107
  onChange={handleChange}
@@ -31,9 +31,9 @@ import {
31
31
  useEditor,
32
32
  useValue,
33
33
  } from '@tldraw/editor'
34
- import { useCallback } from 'react'
34
+ import { useCallback, useContext } from 'react'
35
35
  import { startEditingShapeWithLabel } from '../../tools/SelectTool/selectHelpers'
36
- import { useCurrentTranslation } from '../../ui/hooks/useTranslation/useTranslation'
36
+ import { TranslationsContext } from '../../ui/hooks/useTranslation/useTranslation'
37
37
  import {
38
38
  isEmptyRichText,
39
39
  renderHtmlFromRichTextForMeasurement,
@@ -493,7 +493,8 @@ function getLabelSize(editor: Editor, shape: TLNoteShape) {
493
493
 
494
494
  function useNoteKeydownHandler(id: TLShapeId) {
495
495
  const editor = useEditor()
496
- const translation = useCurrentTranslation()
496
+ // Try to get the translation context, but fallback to ltr if it doesn't exist
497
+ const translation = useContext(TranslationsContext)
497
498
 
498
499
  return useCallback(
499
500
  (e: KeyboardEvent) => {
@@ -512,7 +513,7 @@ function useNoteKeydownHandler(id: TLShapeId) {
512
513
  // tab controls x axis (shift inverts direction set by RTL)
513
514
  // cmd enter is the y axis (shift inverts direction)
514
515
  const isRTL = !!(
515
- translation.dir === 'rtl' ||
516
+ translation?.dir === 'rtl' ||
516
517
  // todo: can we check a partial of the text, so that we don't have to render the whole thing?
517
518
  isRightToLeftLanguage(renderPlaintextFromRichText(editor, shape.props.richText))
518
519
  )
@@ -540,7 +541,7 @@ function useNoteKeydownHandler(id: TLShapeId) {
540
541
  }
541
542
  }
542
543
  },
543
- [id, editor, translation.dir]
544
+ [id, editor, translation?.dir]
544
545
  )
545
546
  }
546
547
 
@@ -3,7 +3,6 @@ import {
3
3
  TLPageId,
4
4
  releasePointerCapture,
5
5
  setPointerCapture,
6
- tlenv,
7
6
  useEditor,
8
7
  useValue,
9
8
  } from '@tldraw/editor'
@@ -306,6 +305,8 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
306
305
  [editor, trackEvent]
307
306
  )
308
307
 
308
+ const shouldUseWindowPrompt = breakpoint < PORTRAIT_BREAKPOINT.TABLET_SM && isCoarsePointer
309
+
309
310
  return (
310
311
  <TldrawUiPopover id="pages" onOpenChange={onOpenChange} open={isOpen}>
311
312
  <TldrawUiPopoverTrigger data-testid="main.page-menu">
@@ -390,7 +391,7 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
390
391
  >
391
392
  <TldrawUiButtonIcon icon="drag-handle-dots" />
392
393
  </TldrawUiButton>
393
- {breakpoint < PORTRAIT_BREAKPOINT.TABLET_SM && isCoarsePointer ? (
394
+ {shouldUseWindowPrompt ? (
394
395
  // sigh, this is a workaround for iOS Safari
395
396
  // because the device and the radix popover seem
396
397
  // to be fighting over scroll position. Nothing
@@ -399,7 +400,7 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
399
400
  type="normal"
400
401
  className="tlui-page-menu__item__button"
401
402
  onClick={() => {
402
- const name = window.prompt('Rename page', page.name)
403
+ const name = window.prompt(msg('action.rename'), page.name)
403
404
  if (name && name !== page.name) {
404
405
  renamePage(page.id, name)
405
406
  }
@@ -465,8 +466,8 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
465
466
  item={page}
466
467
  listSize={pages.length}
467
468
  onRename={() => {
468
- if (tlenv.isIos) {
469
- const name = window.prompt('Rename page', page.name)
469
+ if (shouldUseWindowPrompt) {
470
+ const name = window.prompt(msg('action.rename'), page.name)
470
471
  if (name && name !== page.name) {
471
472
  renamePage(page.id, name)
472
473
  }
@@ -23,7 +23,8 @@ export interface TLUiTranslationProviderProps {
23
23
  /** @public */
24
24
  export type TLUiTranslationContextType = TLUiTranslation
25
25
 
26
- const TranslationsContext = React.createContext<TLUiTranslationContextType | null>(null)
26
+ /** @internal */
27
+ export const TranslationsContext = React.createContext<TLUiTranslationContextType | null>(null)
27
28
 
28
29
  /** @public */
29
30
  export function useCurrentTranslation() {
@@ -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 = '4.2.0-canary.08ca3971cdee'
4
+ export const version = '4.2.0-canary.0ebe44a13b81'
5
5
  export const publishDates = {
6
6
  major: '2025-09-18T14:39:22.803Z',
7
- minor: '2025-11-03T08:49:30.813Z',
8
- patch: '2025-11-03T08:49:30.813Z',
7
+ minor: '2025-11-07T15:16:20.925Z',
8
+ patch: '2025-11-07T15:16:20.925Z',
9
9
  }
@@ -6,16 +6,17 @@ import {
6
6
  HTMLContainer,
7
7
  TLAssetStore,
8
8
  TLBaseShape,
9
+ TLShapeId,
9
10
  TldrawEditor,
10
11
  createShapeId,
11
12
  createTLStore,
12
13
  noop,
14
+ toRichText,
13
15
  } from '@tldraw/editor'
14
16
  import { StrictMode } from 'react'
15
17
  import { vi } from 'vitest'
16
18
  import { defaultShapeUtils } from '../lib/defaultShapeUtils'
17
19
  import { defaultTools } from '../lib/defaultTools'
18
- import { GeoShapeUtil } from '../lib/shapes/geo/GeoShapeUtil'
19
20
  import { defaultAddFontsFromNode, tipTapDefaultExtensions } from '../lib/utils/text/richText'
20
21
  import {
21
22
  renderTldrawComponent,
@@ -169,7 +170,7 @@ describe('<TldrawEditor />', () => {
169
170
  let editor = {} as Editor
170
171
  await renderTldrawComponent(
171
172
  <TldrawEditor
172
- shapeUtils={[GeoShapeUtil]}
173
+ shapeUtils={defaultShapeUtils}
173
174
  initialState="select"
174
175
  tools={defaultTools}
175
176
  onMount={(editorApp) => {
@@ -185,39 +186,83 @@ describe('<TldrawEditor />', () => {
185
186
  editor.updateInstanceState({ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } })
186
187
  })
187
188
 
188
- const id = createShapeId()
189
-
190
- await act(async () => {
191
- editor.createShapes([
192
- {
193
- id,
194
- type: 'geo',
195
- props: { w: 100, h: 100 },
189
+ // Test all shape types except group
190
+ const shapeTypesToTest = [
191
+ { type: 'arrow' as const, props: { start: { x: 0, y: 0 }, end: { x: 100, y: 100 } } },
192
+ { type: 'bookmark' as const, props: { w: 100, h: 100, url: 'https://example.com' } },
193
+ {
194
+ type: 'draw' as const,
195
+ props: { segments: [{ type: 'free' as const, points: [{ x: 0, y: 0, z: 0.5 }] }] },
196
+ },
197
+ { type: 'embed' as const, props: { w: 100, h: 100, url: 'https://example.com' } },
198
+ { type: 'frame' as const, props: { w: 100, h: 100 } },
199
+ { type: 'geo' as const, props: { w: 100, h: 100, geo: 'rectangle' } },
200
+ {
201
+ type: 'highlight' as const,
202
+ props: { segments: [{ type: 'free' as const, points: [{ x: 0, y: 0, z: 0.5 }] }] },
203
+ },
204
+ { type: 'image' as const, props: { w: 100, h: 100 } },
205
+ {
206
+ type: 'line' as const,
207
+ props: {
208
+ points: {
209
+ a1: { id: 'a1', index: 'a1', x: 0, y: 0 },
210
+ a2: { id: 'a2', index: 'a2', x: 100, y: 100 },
211
+ },
196
212
  },
197
- ])
198
- })
213
+ },
214
+ { type: 'note' as const, props: { richText: toRichText('test') } },
215
+ { type: 'text' as const, props: { w: 100, richText: toRichText('test') } },
216
+ { type: 'video' as const, props: { w: 100, h: 100 } },
217
+ ]
218
+
219
+ const shapeIds: TLShapeId[] = []
220
+
221
+ for (let i = 0; i < shapeTypesToTest.length; i++) {
222
+ const shapeConfig = shapeTypesToTest[i]
223
+ const id = createShapeId()
224
+ shapeIds.push(id)
225
+
226
+ await act(async () => {
227
+ editor.createShapes([
228
+ {
229
+ id,
230
+ type: shapeConfig.type,
231
+ x: i * 150, // Space them out horizontally
232
+ y: 0,
233
+ props: shapeConfig.props,
234
+ },
235
+ ])
236
+ })
237
+
238
+ // Does the shape exist?
239
+ const shape = editor.getShape(id)
240
+ expect(shape).toBeTruthy()
241
+ expect(shape?.type).toBe(shapeConfig.type)
242
+
243
+ // Check that all shapes rendered without error boundaries
244
+ expect(
245
+ document.querySelectorAll('.tl-shape-error-boundary'),
246
+ `${shapeConfig.type} had an error while rendering`
247
+ ).toHaveLength(0)
248
+ }
199
249
 
200
- // Does the shape exist?
201
- expect(editor.getShape(id)).toMatchObject({
202
- id,
203
- type: 'geo',
204
- x: 0,
205
- y: 0,
206
- opacity: 1,
207
- props: { geo: 'rectangle', w: 100, h: 100 },
208
- })
250
+ // Check that all shape components are rendering
251
+ expect(document.querySelectorAll('.tl-shape').length).toBeGreaterThanOrEqual(
252
+ shapeTypesToTest.length
253
+ )
209
254
 
210
- // Is the shape's component rendering?
211
- expect(document.querySelectorAll('.tl-shape')).toHaveLength(1)
212
- // though indicator should be display none
213
- expect(document.querySelectorAll('.tl-shape-indicator')).toHaveLength(1)
255
+ // Check that all shape indicators are present
256
+ expect(document.querySelectorAll('.tl-shape-indicator').length).toBeGreaterThanOrEqual(
257
+ shapeTypesToTest.length
258
+ )
214
259
 
215
- // Select the shape
216
- await act(async () => editor.select(id))
260
+ // Select one of the shapes (the note shape)
261
+ const noteShapeId = shapeIds[9] // note is at index 9
262
+ await act(async () => editor.select(noteShapeId))
217
263
 
218
264
  expect(editor.getSelectedShapeIds().length).toBe(1)
219
- // though indicator it should be visible
220
- expect(document.querySelectorAll('.tl-shape-indicator')).toHaveLength(1)
265
+ expect(editor.getSelectedShapeIds()[0]).toBe(noteShapeId)
221
266
 
222
267
  // Select the eraser tool...
223
268
  await act(async () => editor.setCurrentTool('eraser'))