Skip to content

Commit 7308566

Browse files
authored
editor: be able to use TldrawEditor without needing TldrawUiContextProvider (tldraw#7053)
if you wanted to use TldrawEditor by itself, you weren't able to use some tldraw shapes b/c they relied on `TldrawUiContextProvider` being present which could bloat your bundle size unnecessarily if you didn't need all that extra stuff H/T to @derekcicerone for flagging this one — thanks! ### Change type - [x] `bugfix` - [ ] `improvement` - [ ] `feature` - [ ] `api` - [ ] `other` ### Test plan - [x] Unit tests - [ ] End to end tests ### Release notes - editor: be able to use TldrawEditor without needing `TldrawUiContextProvider` <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Make note shape RTL logic resilient to missing UI translation context so TldrawEditor works standalone, add CSS.supports polyfill, and expand shape rendering tests. > > - **Editor / Shapes**: > - Update `NoteShapeUtil` to use optional `TranslationsContext` (fallback to LTR) for RTL/tab navigation, avoiding hard dependency on `TldrawUiContextProvider`. > - **UI / Translations**: > - Export `TranslationsContext` from `ui/hooks/useTranslation/useTranslation` (marked internal). > - **Tests**: > - Revamp `TldrawEditor.test.tsx` to render and validate all core shapes (no error boundaries), use `defaultShapeUtils`, and test selection/tool switching with rich text helpers. > - **Tooling / Polyfills**: > - Add `CSS.supports` polyfill in `internal/config/vitest/setup.ts` for color space-related tests. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit da964bd. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 8e28283 commit 7308566

4 files changed

Lines changed: 90 additions & 35 deletions

File tree

internal/config/vitest/setup.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ if (typeof HTMLImageElement !== 'undefined') {
5050
}
5151
}
5252

53+
// CSS.supports polyfill for tests that use color spaces (e.g., highlight shapes)
54+
if (typeof CSS === 'undefined') {
55+
;(global as any).CSS = {}
56+
}
57+
if (typeof CSS.supports === 'undefined') {
58+
CSS.supports = () => false
59+
}
60+
5361
function convertNumbersInObject(obj: any, roundToNearest: number): any {
5462
if (!obj) return obj
5563
if (Array.isArray(obj)) {

packages/tldraw/src/lib/shapes/note/NoteShapeUtil.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ import {
3131
useEditor,
3232
useValue,
3333
} from '@tldraw/editor'
34-
import { useCallback } from 'react'
34+
import { useCallback, useContext } from 'react'
3535
import { startEditingShapeWithLabel } from '../../tools/SelectTool/selectHelpers'
36-
import { useCurrentTranslation } from '../../ui/hooks/useTranslation/useTranslation'
36+
import { TranslationsContext } from '../../ui/hooks/useTranslation/useTranslation'
3737
import {
3838
isEmptyRichText,
3939
renderHtmlFromRichTextForMeasurement,
@@ -493,7 +493,8 @@ function getLabelSize(editor: Editor, shape: TLNoteShape) {
493493

494494
function useNoteKeydownHandler(id: TLShapeId) {
495495
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)
497498

498499
return useCallback(
499500
(e: KeyboardEvent) => {
@@ -512,7 +513,7 @@ function useNoteKeydownHandler(id: TLShapeId) {
512513
// tab controls x axis (shift inverts direction set by RTL)
513514
// cmd enter is the y axis (shift inverts direction)
514515
const isRTL = !!(
515-
translation.dir === 'rtl' ||
516+
translation?.dir === 'rtl' ||
516517
// todo: can we check a partial of the text, so that we don't have to render the whole thing?
517518
isRightToLeftLanguage(renderPlaintextFromRichText(editor, shape.props.richText))
518519
)
@@ -540,7 +541,7 @@ function useNoteKeydownHandler(id: TLShapeId) {
540541
}
541542
}
542543
},
543-
[id, editor, translation.dir]
544+
[id, editor, translation?.dir]
544545
)
545546
}
546547

packages/tldraw/src/lib/ui/hooks/useTranslation/useTranslation.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ export interface TLUiTranslationProviderProps {
2323
/** @public */
2424
export type TLUiTranslationContextType = TLUiTranslation
2525

26-
const TranslationsContext = React.createContext<TLUiTranslationContextType | null>(null)
26+
/** @internal */
27+
export const TranslationsContext = React.createContext<TLUiTranslationContextType | null>(null)
2728

2829
/** @public */
2930
export function useCurrentTranslation() {

packages/tldraw/src/test/TldrawEditor.test.tsx

Lines changed: 74 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,17 @@ import {
66
HTMLContainer,
77
TLAssetStore,
88
TLBaseShape,
9+
TLShapeId,
910
TldrawEditor,
1011
createShapeId,
1112
createTLStore,
1213
noop,
14+
toRichText,
1315
} from '@tldraw/editor'
1416
import { StrictMode } from 'react'
1517
import { vi } from 'vitest'
1618
import { defaultShapeUtils } from '../lib/defaultShapeUtils'
1719
import { defaultTools } from '../lib/defaultTools'
18-
import { GeoShapeUtil } from '../lib/shapes/geo/GeoShapeUtil'
1920
import { defaultAddFontsFromNode, tipTapDefaultExtensions } from '../lib/utils/text/richText'
2021
import {
2122
renderTldrawComponent,
@@ -169,7 +170,7 @@ describe('<TldrawEditor />', () => {
169170
let editor = {} as Editor
170171
await renderTldrawComponent(
171172
<TldrawEditor
172-
shapeUtils={[GeoShapeUtil]}
173+
shapeUtils={defaultShapeUtils}
173174
initialState="select"
174175
tools={defaultTools}
175176
onMount={(editorApp) => {
@@ -185,39 +186,83 @@ describe('<TldrawEditor />', () => {
185186
editor.updateInstanceState({ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } })
186187
})
187188

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+
},
196212
},
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+
}
199249

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+
)
209254

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+
)
214259

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))
217263

218264
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)
221266

222267
// Select the eraser tool...
223268
await act(async () => editor.setCurrentTool('eraser'))

0 commit comments

Comments
 (0)