tldraw 5.3.1 → 5.4.0-canary.042da660fa66
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.
- package/DOCS.md +2822 -3224
- package/RELEASE_NOTES.md +21 -33
- package/dist-cjs/index.d.ts +264 -275
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/defaultExternalContentHandlers.js +2 -2
- package/dist-cjs/lib/defaultExternalContentHandlers.js.map +2 -2
- package/dist-cjs/lib/shapes/draw/toolStates/Drawing.js +2 -2
- package/dist-cjs/lib/shapes/draw/toolStates/Drawing.js.map +2 -2
- package/dist-cjs/lib/shapes/frame/frameHelpers.js +3 -0
- package/dist-cjs/lib/shapes/frame/frameHelpers.js.map +2 -2
- package/dist-cjs/lib/shapes/video/VideoShapeUtil.js +3 -2
- package/dist-cjs/lib/shapes/video/VideoShapeUtil.js.map +2 -2
- package/dist-cjs/lib/ui/TldrawUi.js +2 -3
- package/dist-cjs/lib/ui/TldrawUi.js.map +2 -2
- package/dist-cjs/lib/ui/components/ActionsMenu/DefaultActionsMenu.js +0 -3
- package/dist-cjs/lib/ui/components/ActionsMenu/DefaultActionsMenu.js.map +2 -2
- package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenuContent.js +1 -1
- package/dist-cjs/lib/ui/components/ContextMenu/DefaultContextMenuContent.js.map +2 -2
- package/dist-cjs/lib/ui/components/Dialogs.js +8 -0
- package/dist-cjs/lib/ui/components/Dialogs.js.map +2 -2
- package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuItem.js +1 -2
- package/dist-cjs/lib/ui/components/SharePanel/DefaultPeopleMenuItem.js.map +2 -2
- package/dist-cjs/lib/ui/context/actions.js +0 -1
- package/dist-cjs/lib/ui/context/actions.js.map +2 -2
- package/dist-cjs/lib/ui/hooks/useClipboardEvents.js +2 -0
- package/dist-cjs/lib/ui/hooks/useClipboardEvents.js.map +2 -2
- package/dist-cjs/lib/ui/hooks/useCommentingEnabled.js +1 -1
- package/dist-cjs/lib/ui/hooks/useCommentingEnabled.js.map +2 -2
- package/dist-cjs/lib/ui/hooks/useFlatten.js +42 -23
- package/dist-cjs/lib/ui/hooks/useFlatten.js.map +2 -2
- package/dist-cjs/lib/ui/hooks/useKeyboardShortcuts.js +5 -17
- package/dist-cjs/lib/ui/hooks/useKeyboardShortcuts.js.map +2 -2
- package/dist-cjs/lib/ui/kbd-utils.js +22 -3
- package/dist-cjs/lib/ui/kbd-utils.js.map +2 -2
- package/dist-cjs/lib/ui/version.js +3 -3
- package/dist-cjs/lib/ui/version.js.map +1 -1
- package/dist-esm/index.d.mts +264 -275
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/defaultExternalContentHandlers.mjs +2 -2
- package/dist-esm/lib/defaultExternalContentHandlers.mjs.map +2 -2
- package/dist-esm/lib/shapes/draw/toolStates/Drawing.mjs +2 -2
- package/dist-esm/lib/shapes/draw/toolStates/Drawing.mjs.map +2 -2
- package/dist-esm/lib/shapes/frame/frameHelpers.mjs +3 -0
- package/dist-esm/lib/shapes/frame/frameHelpers.mjs.map +2 -2
- package/dist-esm/lib/shapes/video/VideoShapeUtil.mjs +3 -2
- package/dist-esm/lib/shapes/video/VideoShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/ui/TldrawUi.mjs +2 -3
- package/dist-esm/lib/ui/TldrawUi.mjs.map +2 -2
- package/dist-esm/lib/ui/components/ActionsMenu/DefaultActionsMenu.mjs +2 -5
- package/dist-esm/lib/ui/components/ActionsMenu/DefaultActionsMenu.mjs.map +2 -2
- package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenuContent.mjs +1 -1
- package/dist-esm/lib/ui/components/ContextMenu/DefaultContextMenuContent.mjs.map +2 -2
- package/dist-esm/lib/ui/components/Dialogs.mjs +9 -1
- package/dist-esm/lib/ui/components/Dialogs.mjs.map +2 -2
- package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuItem.mjs +1 -2
- package/dist-esm/lib/ui/components/SharePanel/DefaultPeopleMenuItem.mjs.map +2 -2
- package/dist-esm/lib/ui/context/actions.mjs +0 -1
- package/dist-esm/lib/ui/context/actions.mjs.map +2 -2
- package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs +2 -0
- package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs.map +2 -2
- package/dist-esm/lib/ui/hooks/useCommentingEnabled.mjs +2 -2
- package/dist-esm/lib/ui/hooks/useCommentingEnabled.mjs.map +2 -2
- package/dist-esm/lib/ui/hooks/useFlatten.mjs +42 -23
- package/dist-esm/lib/ui/hooks/useFlatten.mjs.map +2 -2
- package/dist-esm/lib/ui/hooks/useKeyboardShortcuts.mjs +5 -17
- package/dist-esm/lib/ui/hooks/useKeyboardShortcuts.mjs.map +2 -2
- package/dist-esm/lib/ui/kbd-utils.mjs +22 -3
- package/dist-esm/lib/ui/kbd-utils.mjs.map +2 -2
- package/dist-esm/lib/ui/version.mjs +3 -3
- package/dist-esm/lib/ui/version.mjs.map +1 -1
- package/package.json +12 -6
- package/src/lib/defaultExternalContentHandlers.ts +2 -2
- package/src/lib/shapes/draw/DrawShapeTool.test.ts +19 -0
- package/src/lib/shapes/draw/toolStates/Drawing.ts +2 -2
- package/src/lib/shapes/frame/frameHelpers.ts +10 -1
- package/src/lib/shapes/video/VideoShapeUtil.tsx +4 -2
- package/src/lib/ui/TldrawUi.tsx +1 -2
- package/src/lib/ui/components/ActionsMenu/DefaultActionsMenu.tsx +2 -6
- package/src/lib/ui/components/ContextMenu/DefaultContextMenuContent.tsx +5 -1
- package/src/lib/ui/components/Dialogs.tsx +17 -1
- package/src/lib/ui/components/SharePanel/DefaultPeopleMenuItem.tsx +2 -3
- package/src/lib/ui/context/actions.tsx +0 -1
- package/src/lib/ui/hooks/useClipboardEvents.ts +2 -0
- package/src/lib/ui/hooks/useCommentingEnabled.ts +5 -3
- package/src/lib/ui/hooks/useFlatten.ts +56 -23
- package/src/lib/ui/hooks/useKeyboardShortcuts.ts +5 -23
- package/src/lib/ui/kbd-utils.test.ts +11 -0
- package/src/lib/ui/kbd-utils.ts +27 -4
- package/src/lib/ui/version.ts +3 -3
- package/src/lib/ui.css +7 -16
- package/src/test/TldrawEditor.test.tsx +3 -0
- package/src/test/VideoShapeUtil.test.tsx +126 -0
- package/src/test/bookmark-shapes.test.ts +110 -0
- package/src/test/commands/alignShapes.test.tsx +19 -10
- package/src/test/commands/clipboard.test.ts +36 -1
- package/src/test/flatten.test.ts +135 -0
- package/src/test/modifiers.test.ts +104 -0
- package/src/test/ui-outside-editor.test.tsx +74 -0
- package/tldraw.css +7 -16
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AssetRecordType,
|
|
3
|
+
TLImageAsset,
|
|
4
|
+
TLShapeId,
|
|
5
|
+
TLShapePartial,
|
|
6
|
+
createShapeId,
|
|
7
|
+
} from '@tldraw/editor'
|
|
8
|
+
import { vi } from 'vitest'
|
|
9
|
+
import { flattenShapesToImages } from '../lib/ui/hooks/useFlatten'
|
|
10
|
+
import { TestEditor } from './TestEditor'
|
|
11
|
+
|
|
12
|
+
let editor: TestEditor
|
|
13
|
+
let assetCount = 0
|
|
14
|
+
|
|
15
|
+
function installFlattenMocks(target: TestEditor) {
|
|
16
|
+
// flattenShapesToImages calls editor.getSvgString and
|
|
17
|
+
// editor.getAssetForExternalContent. Mock both to bypass the real export
|
|
18
|
+
// pipeline (jsdom can't render SVGs and we don't need real pixels — these
|
|
19
|
+
// tests only care about which shapes end up in which group).
|
|
20
|
+
vi.spyOn(target, 'getSvgString').mockResolvedValue({
|
|
21
|
+
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"></svg>',
|
|
22
|
+
width: 10,
|
|
23
|
+
height: 10,
|
|
24
|
+
trimPadding: 0,
|
|
25
|
+
})
|
|
26
|
+
vi.spyOn(target, 'getAssetForExternalContent').mockImplementation(async () => {
|
|
27
|
+
const asset: TLImageAsset = {
|
|
28
|
+
id: AssetRecordType.createId(`flat-${assetCount++}`),
|
|
29
|
+
typeName: 'asset',
|
|
30
|
+
type: 'image',
|
|
31
|
+
props: {
|
|
32
|
+
w: 10,
|
|
33
|
+
h: 10,
|
|
34
|
+
name: 'asset.svg',
|
|
35
|
+
isAnimated: false,
|
|
36
|
+
mimeType: 'image/svg+xml',
|
|
37
|
+
src: 'data:image/svg+xml;base64,',
|
|
38
|
+
},
|
|
39
|
+
meta: {},
|
|
40
|
+
}
|
|
41
|
+
return asset
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
editor = new TestEditor()
|
|
47
|
+
assetCount = 0
|
|
48
|
+
installFlattenMocks(editor)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
editor?.dispose()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const box = (id: TLShapeId, x: number, y: number, w = 50, h = 50): TLShapePartial => ({
|
|
56
|
+
id,
|
|
57
|
+
type: 'geo',
|
|
58
|
+
x,
|
|
59
|
+
y,
|
|
60
|
+
props: { w, h, fill: 'solid' },
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
function getImageShapeCount() {
|
|
64
|
+
return editor.getCurrentPageShapes().filter((s) => s.type === 'image').length
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe('flattenShapesToImages clustering', () => {
|
|
68
|
+
const expand = 64
|
|
69
|
+
|
|
70
|
+
it('groups shapes within the expand threshold into one image', async () => {
|
|
71
|
+
const a = createShapeId('a')
|
|
72
|
+
const b = createShapeId('b')
|
|
73
|
+
// gap of 30 < 2 * 64 expand, so the expanded boxes intersect
|
|
74
|
+
editor.createShapes([box(a, 0, 0), box(b, 80, 0)])
|
|
75
|
+
|
|
76
|
+
await flattenShapesToImages(editor, [a, b], expand)
|
|
77
|
+
|
|
78
|
+
expect(getImageShapeCount()).toBe(1)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('keeps shapes in separate images when their gap exceeds the expand threshold', async () => {
|
|
82
|
+
const a = createShapeId('a')
|
|
83
|
+
const b = createShapeId('b')
|
|
84
|
+
// gap of 200 > 2 * 64 expand, so the expanded boxes do not intersect
|
|
85
|
+
editor.createShapes([box(a, 0, 0), box(b, 250, 0)])
|
|
86
|
+
|
|
87
|
+
await flattenShapesToImages(editor, [a, b], expand)
|
|
88
|
+
|
|
89
|
+
expect(getImageShapeCount()).toBe(2)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('merges all shapes into one group when a bridging shape connects them, independent of input order', async () => {
|
|
93
|
+
// Layout: A — gap — bridge — gap — C. A and C alone do not intersect
|
|
94
|
+
// each other's expanded bounds, but `bridge` sits in the middle and
|
|
95
|
+
// should pull them all into a single group regardless of where it
|
|
96
|
+
// appears in the list. The previous greedy pass would only merge
|
|
97
|
+
// `bridge` into the first group it touched, leaving C as a separate
|
|
98
|
+
// cluster when `bridge` came after C in the list — which made the
|
|
99
|
+
// result depend on selection order.
|
|
100
|
+
const a = createShapeId('a')
|
|
101
|
+
const bridge = createShapeId('bridge')
|
|
102
|
+
const c = createShapeId('c')
|
|
103
|
+
|
|
104
|
+
const orders: TLShapeId[][] = [
|
|
105
|
+
[a, bridge, c],
|
|
106
|
+
[a, c, bridge],
|
|
107
|
+
[bridge, a, c],
|
|
108
|
+
[c, bridge, a],
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
for (const order of orders) {
|
|
112
|
+
editor.dispose()
|
|
113
|
+
editor = new TestEditor()
|
|
114
|
+
assetCount = 0
|
|
115
|
+
installFlattenMocks(editor)
|
|
116
|
+
editor.createShapes([box(a, 0, 0), box(bridge, 100, 0), box(c, 200, 0)])
|
|
117
|
+
|
|
118
|
+
await flattenShapesToImages(editor, order, expand)
|
|
119
|
+
|
|
120
|
+
expect(getImageShapeCount(), `order ${order.join(',')}`).toBe(1)
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('uses a single Box.Common group when no expand value is provided', async () => {
|
|
125
|
+
// Two shapes far apart should still flatten to one image when called
|
|
126
|
+
// via the no-expand path (the default useFlatten() hook).
|
|
127
|
+
const a = createShapeId('a')
|
|
128
|
+
const b = createShapeId('b')
|
|
129
|
+
editor.createShapes([box(a, 0, 0), box(b, 1000, 0)])
|
|
130
|
+
|
|
131
|
+
await flattenShapesToImages(editor, [a, b])
|
|
132
|
+
|
|
133
|
+
expect(getImageShapeCount()).toBe(1)
|
|
134
|
+
})
|
|
135
|
+
})
|
|
@@ -37,6 +37,110 @@ it('Ctrl Key', () => {
|
|
|
37
37
|
expect(editor.inputs.getCtrlKey()).toBe(false)
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
+
it('pointer down releases a modifier still in its debounce window', () => {
|
|
41
|
+
// Collect any synthetic key_up events fired when the release is flushed.
|
|
42
|
+
const keyUps: string[] = []
|
|
43
|
+
editor.on('event', (info) => {
|
|
44
|
+
if (info.type === 'keyboard' && info.name === 'key_up') keyUps.push(info.key)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// Hold then release meta via pointer events; the release is debounced for 150ms.
|
|
48
|
+
editor.pointerMove(100, 100, { metaKey: true })
|
|
49
|
+
editor.pointerMove(100, 100, { metaKey: false })
|
|
50
|
+
expect(editor.inputs.getMetaKey()).toBe(true) // still held during the debounce window
|
|
51
|
+
|
|
52
|
+
// A new interaction starts with the key physically up: the lingering modifier
|
|
53
|
+
// should be released immediately (same path as the timer), including its
|
|
54
|
+
// synthetic key_up, without waiting for the timeout.
|
|
55
|
+
editor.pointerDown(100, 100, { metaKey: false })
|
|
56
|
+
expect(editor.inputs.getMetaKey()).toBe(false)
|
|
57
|
+
expect(keyUps).toEqual(['Meta'])
|
|
58
|
+
|
|
59
|
+
// The pending timer must have been cancelled, so advancing past it does nothing
|
|
60
|
+
// and doesn't fire the synthetic key_up a second time.
|
|
61
|
+
vi.advanceTimersByTime(200)
|
|
62
|
+
expect(editor.inputs.getMetaKey()).toBe(false)
|
|
63
|
+
expect(keyUps).toEqual(['Meta'])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('pointer down releases every modifier still in its debounce window', () => {
|
|
67
|
+
const keyUps: string[] = []
|
|
68
|
+
editor.on('event', (info) => {
|
|
69
|
+
if (info.type === 'keyboard' && info.name === 'key_up') keyUps.push(info.key)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
editor.pointerMove(100, 100, { shiftKey: true, altKey: true, ctrlKey: true, metaKey: true })
|
|
73
|
+
editor.pointerMove(100, 100, { shiftKey: false, altKey: false, ctrlKey: false, metaKey: false })
|
|
74
|
+
expect(editor.inputs.getShiftKey()).toBe(true)
|
|
75
|
+
expect(editor.inputs.getAltKey()).toBe(true)
|
|
76
|
+
expect(editor.inputs.getCtrlKey()).toBe(true)
|
|
77
|
+
expect(editor.inputs.getMetaKey()).toBe(true)
|
|
78
|
+
|
|
79
|
+
editor.pointerDown(100, 100, { shiftKey: false, altKey: false, ctrlKey: false, metaKey: false })
|
|
80
|
+
expect(editor.inputs.getShiftKey()).toBe(false)
|
|
81
|
+
expect(editor.inputs.getAltKey()).toBe(false)
|
|
82
|
+
expect(editor.inputs.getCtrlKey()).toBe(false)
|
|
83
|
+
expect(editor.inputs.getMetaKey()).toBe(false)
|
|
84
|
+
|
|
85
|
+
// Each flushed modifier fires its synthetic key_up so tools can react.
|
|
86
|
+
expect(keyUps).toEqual(expect.arrayContaining(['Shift', 'Alt', 'Ctrl', 'Meta']))
|
|
87
|
+
expect(keyUps).toHaveLength(4)
|
|
88
|
+
|
|
89
|
+
// All four timers were cancelled, so nothing flips back or fires again.
|
|
90
|
+
vi.advanceTimersByTime(200)
|
|
91
|
+
expect(editor.inputs.getShiftKey()).toBe(false)
|
|
92
|
+
expect(editor.inputs.getAltKey()).toBe(false)
|
|
93
|
+
expect(editor.inputs.getCtrlKey()).toBe(false)
|
|
94
|
+
expect(editor.inputs.getMetaKey()).toBe(false)
|
|
95
|
+
expect(keyUps).toHaveLength(4)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('pointer down clears stale key codes when flushing a debounced modifier', () => {
|
|
99
|
+
// A real key_down records the code, then the release is only seen via pointer
|
|
100
|
+
// flags (no keyboard key_up), so the code lingers in inputs.keys until flushed.
|
|
101
|
+
editor.keyDown('Shift')
|
|
102
|
+
expect(editor.inputs.keys.has('ShiftLeft')).toBe(true)
|
|
103
|
+
|
|
104
|
+
editor.pointerMove(100, 100, { shiftKey: false })
|
|
105
|
+
expect(editor.inputs.getShiftKey()).toBe(true) // debounced, still counted as held
|
|
106
|
+
expect(editor.inputs.keys.has('ShiftLeft')).toBe(true) // code not yet cleared
|
|
107
|
+
|
|
108
|
+
// The pointer down flushes the debounce through the timer's path, which clears
|
|
109
|
+
// both the modifier atom and the stale code.
|
|
110
|
+
editor.pointerDown(100, 100, { shiftKey: false })
|
|
111
|
+
expect(editor.inputs.getShiftKey()).toBe(false)
|
|
112
|
+
expect(editor.inputs.keys.has('ShiftLeft')).toBe(false)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('does not re-release a modifier after its natural debounce timer fired', () => {
|
|
116
|
+
const keyUps: string[] = []
|
|
117
|
+
editor.on('event', (info) => {
|
|
118
|
+
if (info.type === 'keyboard' && info.name === 'key_up') keyUps.push(info.key)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// Hold then release shift via pointer events and let the 150ms timer fire on its
|
|
122
|
+
// own, which releases shift with a single synthetic key_up.
|
|
123
|
+
editor.pointerMove(100, 100, { shiftKey: true })
|
|
124
|
+
editor.pointerMove(100, 100, { shiftKey: false })
|
|
125
|
+
vi.advanceTimersByTime(200)
|
|
126
|
+
expect(editor.inputs.getShiftKey()).toBe(false)
|
|
127
|
+
expect(keyUps).toEqual(['Shift'])
|
|
128
|
+
|
|
129
|
+
// A later pointer down must not treat the already-fired timer as an active
|
|
130
|
+
// debounce and emit another synthetic key_up.
|
|
131
|
+
editor.pointerDown(100, 100, { shiftKey: false })
|
|
132
|
+
expect(keyUps).toEqual(['Shift'])
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('pointer down keeps a genuinely held modifier', () => {
|
|
136
|
+
// Shift is actually held during the pointer down (flag stays true), so it must
|
|
137
|
+
// not be cleared by the debounce-flush.
|
|
138
|
+
editor.pointerMove(100, 100, { shiftKey: true })
|
|
139
|
+
expect(editor.inputs.getShiftKey()).toBe(true)
|
|
140
|
+
editor.pointerDown(100, 100, { shiftKey: true })
|
|
141
|
+
expect(editor.inputs.getShiftKey()).toBe(true)
|
|
142
|
+
})
|
|
143
|
+
|
|
40
144
|
it('keyboard events carry currently-held modifiers', () => {
|
|
41
145
|
const keyboardEvents: TLKeyboardEventInfo[] = []
|
|
42
146
|
editor.on('event', (info) => {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { render } from '@testing-library/react'
|
|
2
|
+
import { ContainerProvider, EditorProvider, useLicenseContext } from '@tldraw/editor'
|
|
3
|
+
import { vi } from 'vitest'
|
|
4
|
+
import { Tldraw } from '../lib/Tldraw'
|
|
5
|
+
import { DefaultToolbar } from '../lib/ui/components/Toolbar/DefaultToolbar'
|
|
6
|
+
import { TldrawUiContextProvider } from '../lib/ui/context/TldrawUiContextProvider'
|
|
7
|
+
import { useCommentingEnabled } from '../lib/ui/hooks/useCommentingEnabled'
|
|
8
|
+
import { renderTldrawComponentWithEditor } from './testutils/renderTldrawComponent'
|
|
9
|
+
|
|
10
|
+
// Default UI components can be mounted beside <Tldraw /> rather than inside it, wrapped in the
|
|
11
|
+
// providers the caller recreates (container, editor, ui context). The license provider is not
|
|
12
|
+
// among them — it only exists inside <TldrawEditor />.
|
|
13
|
+
describe('default UI mounted outside <Tldraw />', () => {
|
|
14
|
+
it('renders the default toolbar and resolves license feature flags through the editor', async () => {
|
|
15
|
+
const { editor } = await renderTldrawComponentWithEditor(
|
|
16
|
+
(onMount) => <Tldraw onMount={onMount} components={{ Toolbar: null }} />,
|
|
17
|
+
{ waitForPatterns: true }
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
expect(editor.licenseManager).toBeDefined()
|
|
21
|
+
|
|
22
|
+
let commentingEnabled: boolean | undefined
|
|
23
|
+
function CommentingProbe() {
|
|
24
|
+
commentingEnabled = useCommentingEnabled()
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const result = render(
|
|
29
|
+
<ContainerProvider container={editor.getContainer()}>
|
|
30
|
+
<EditorProvider editor={editor}>
|
|
31
|
+
<TldrawUiContextProvider>
|
|
32
|
+
<DefaultToolbar />
|
|
33
|
+
<CommentingProbe />
|
|
34
|
+
</TldrawUiContextProvider>
|
|
35
|
+
</EditorProvider>
|
|
36
|
+
</ContainerProvider>
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
expect(await result.findByTestId('tools.select')).toBeTruthy()
|
|
40
|
+
// In tests the environment counts as development, where every feature is licensed — so a
|
|
41
|
+
// `true` here proves the flag resolved via the editor's license manager, not the
|
|
42
|
+
// fail-closed `false` of having no license manager at all.
|
|
43
|
+
expect(commentingEnabled).toBe(true)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('license feature flags fail closed with no editor and no license context', () => {
|
|
47
|
+
let commentingEnabled: boolean | undefined
|
|
48
|
+
function CommentingProbe() {
|
|
49
|
+
commentingEnabled = useCommentingEnabled()
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
render(<CommentingProbe />)
|
|
54
|
+
|
|
55
|
+
expect(commentingEnabled).toBe(false)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('useLicenseContext throws when there is no license provider and no editor', () => {
|
|
59
|
+
function LicenseProbe() {
|
|
60
|
+
useLicenseContext()
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// silence React's error boundary logging for the expected throw
|
|
65
|
+
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
66
|
+
try {
|
|
67
|
+
expect(() => render(<LicenseProbe />)).toThrow(
|
|
68
|
+
'useLicenseContext must be used inside of the <Tldraw /> or <TldrawEditor /> components'
|
|
69
|
+
)
|
|
70
|
+
} finally {
|
|
71
|
+
consoleError.mockRestore()
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
})
|
package/tldraw.css
CHANGED
|
@@ -3726,7 +3726,8 @@ tldraw? probably.
|
|
|
3726
3726
|
align-items: center;
|
|
3727
3727
|
}
|
|
3728
3728
|
|
|
3729
|
-
.tlui-people-menu__user__color
|
|
3729
|
+
.tlui-people-menu__user__color,
|
|
3730
|
+
.tlui-people-menu__user__edit {
|
|
3730
3731
|
flex-shrink: 0;
|
|
3731
3732
|
}
|
|
3732
3733
|
|
|
@@ -3772,7 +3773,8 @@ tldraw? probably.
|
|
|
3772
3773
|
}
|
|
3773
3774
|
|
|
3774
3775
|
.tlui-people-menu__item {
|
|
3775
|
-
|
|
3776
|
+
display: flex;
|
|
3777
|
+
align-items: center;
|
|
3776
3778
|
}
|
|
3777
3779
|
|
|
3778
3780
|
.tlui-people-menu__item:last-of-type .tlui-button__menu {
|
|
@@ -3780,7 +3782,8 @@ tldraw? probably.
|
|
|
3780
3782
|
}
|
|
3781
3783
|
|
|
3782
3784
|
.tlui-people-menu__item__button {
|
|
3783
|
-
padding:
|
|
3785
|
+
padding-inline-start: 12px;
|
|
3786
|
+
padding-inline-end: 0px;
|
|
3784
3787
|
overflow: hidden;
|
|
3785
3788
|
}
|
|
3786
3789
|
|
|
@@ -3790,7 +3793,7 @@ tldraw? probably.
|
|
|
3790
3793
|
align-items: auto;
|
|
3791
3794
|
justify-content: flex-start;
|
|
3792
3795
|
flex-grow: 2;
|
|
3793
|
-
gap:
|
|
3796
|
+
gap: 12px;
|
|
3794
3797
|
}
|
|
3795
3798
|
|
|
3796
3799
|
.tlui-people-menu__name {
|
|
@@ -3801,18 +3804,9 @@ tldraw? probably.
|
|
|
3801
3804
|
}
|
|
3802
3805
|
|
|
3803
3806
|
.tlui-people-menu__item__follow {
|
|
3804
|
-
position: absolute;
|
|
3805
|
-
top: 0px;
|
|
3806
|
-
inset-inline-end: 0px;
|
|
3807
|
-
max-width: 40px;
|
|
3808
3807
|
flex-shrink: 0;
|
|
3809
3808
|
}
|
|
3810
3809
|
|
|
3811
|
-
.tlui-people-menu__item[data-follow='true'],
|
|
3812
|
-
.tlui-people-menu__item:has(.tlui-button:focus-visible) {
|
|
3813
|
-
padding-inline-end: 36px;
|
|
3814
|
-
}
|
|
3815
|
-
|
|
3816
3810
|
.tlui-people-menu__item[data-follow='true'] .tlui-people-menu__item__follow,
|
|
3817
3811
|
.tlui-people-menu__item:has(.tlui-button:focus-visible) .tlui-people-menu__item__follow {
|
|
3818
3812
|
opacity: 1;
|
|
@@ -3823,9 +3817,6 @@ tldraw? probably.
|
|
|
3823
3817
|
opacity: 0;
|
|
3824
3818
|
}
|
|
3825
3819
|
|
|
3826
|
-
.tlui-people-menu__item:hover {
|
|
3827
|
-
padding-right: 36px;
|
|
3828
|
-
}
|
|
3829
3820
|
.tlui-people-menu__item:hover .tlui-people-menu__item__follow {
|
|
3830
3821
|
opacity: 1;
|
|
3831
3822
|
}
|