@dfosco/storyboard-react 4.0.0-beta.2 → 4.0.0-beta.21
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/package.json +7 -4
- package/src/canvas/CanvasControls.jsx +51 -2
- package/src/canvas/CanvasControls.module.css +31 -0
- package/src/canvas/CanvasPage.dragdrop.test.jsx +346 -0
- package/src/canvas/CanvasPage.jsx +512 -235
- package/src/canvas/CanvasPage.module.css +9 -47
- package/src/canvas/ComponentErrorBoundary.jsx +50 -0
- package/src/canvas/PageSelector.jsx +102 -0
- package/src/canvas/PageSelector.module.css +93 -0
- package/src/canvas/PageSelector.test.jsx +104 -0
- package/src/canvas/canvasApi.js +4 -0
- package/src/canvas/canvasReloadGuard.js +37 -0
- package/src/canvas/canvasReloadGuard.test.js +27 -0
- package/src/canvas/componentIsolate.jsx +135 -0
- package/src/canvas/useCanvas.js +6 -2
- package/src/canvas/widgets/ComponentWidget.jsx +67 -9
- package/src/canvas/widgets/ComponentWidget.module.css +9 -6
- package/src/canvas/widgets/FigmaEmbed.jsx +26 -4
- package/src/canvas/widgets/FigmaEmbed.module.css +0 -7
- package/src/canvas/widgets/MarkdownBlock.jsx +94 -21
- package/src/canvas/widgets/MarkdownBlock.module.css +110 -2
- package/src/canvas/widgets/MarkdownBlock.test.jsx +39 -0
- package/src/canvas/widgets/PrototypeEmbed.jsx +196 -40
- package/src/canvas/widgets/PrototypeEmbed.module.css +30 -3
- package/src/canvas/widgets/StickyNote.module.css +5 -0
- package/src/canvas/widgets/StickyNote.test.jsx +9 -9
- package/src/canvas/widgets/StoryWidget.jsx +471 -0
- package/src/canvas/widgets/StoryWidget.module.css +200 -0
- package/src/canvas/widgets/WidgetChrome.jsx +54 -18
- package/src/canvas/widgets/WidgetChrome.module.css +4 -7
- package/src/canvas/widgets/WidgetWrapper.module.css +2 -0
- package/src/canvas/widgets/embedInteraction.test.jsx +155 -0
- package/src/canvas/widgets/embedOverlay.module.css +35 -0
- package/src/canvas/widgets/index.js +2 -0
- package/src/canvas/widgets/pasteRules.js +295 -0
- package/src/canvas/widgets/pasteRules.test.js +474 -0
- package/src/canvas/widgets/widgetConfig.js +16 -5
- package/src/canvas/widgets/widgetConfig.test.js +31 -9
- package/src/context.jsx +138 -13
- package/src/hooks/useSceneData.js +4 -2
- package/src/story/StoryPage.jsx +152 -0
- package/src/story/StoryPage.module.css +73 -0
- package/src/vite/data-plugin.js +441 -58
- package/src/vite/data-plugin.test.js +405 -5
|
@@ -4,7 +4,9 @@ import { buildPrototypeIndex } from '@dfosco/storyboard-core'
|
|
|
4
4
|
import WidgetWrapper from './WidgetWrapper.jsx'
|
|
5
5
|
import { readProp, prototypeEmbedSchema } from './widgetProps.js'
|
|
6
6
|
import { getEmbedChromeVars } from './embedTheme.js'
|
|
7
|
+
import { uploadImage } from '../canvasApi.js'
|
|
7
8
|
import styles from './PrototypeEmbed.module.css'
|
|
9
|
+
import overlayStyles from './embedOverlay.module.css'
|
|
8
10
|
|
|
9
11
|
function formatName(name) {
|
|
10
12
|
return name
|
|
@@ -29,25 +31,29 @@ function resolveCanvasThemeFromStorage() {
|
|
|
29
31
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
export default forwardRef(function PrototypeEmbed({ props, onUpdate, resizable }, ref) {
|
|
34
|
+
export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdate, resizable }, ref) {
|
|
33
35
|
const src = readProp(props, 'src', prototypeEmbedSchema)
|
|
34
36
|
const width = readProp(props, 'width', prototypeEmbedSchema)
|
|
35
37
|
const height = readProp(props, 'height', prototypeEmbedSchema)
|
|
36
38
|
const zoom = readProp(props, 'zoom', prototypeEmbedSchema)
|
|
37
39
|
const label = readProp(props, 'label', prototypeEmbedSchema) || src
|
|
38
40
|
|
|
41
|
+
// Snapshot props for lazy loading
|
|
42
|
+
const snapshotLight = props?.snapshotLight || null
|
|
43
|
+
const snapshotDark = props?.snapshotDark || null
|
|
44
|
+
|
|
39
45
|
const basePath = (import.meta.env.BASE_URL || '/').replace(/\/$/, '')
|
|
40
46
|
const baseSegment = basePath.replace(/^\//, '')
|
|
41
47
|
const rawSrc = useMemo(() => {
|
|
42
48
|
if (!src) return ''
|
|
43
49
|
if (/^https?:\/\//.test(src)) return src
|
|
44
|
-
// Strip stale branch prefixes from stored src (e.g. /branch--old-feat/Page)
|
|
45
50
|
const cleaned = src.replace(/^\/branch--[^/]+/, '')
|
|
46
51
|
if (baseSegment && cleaned.startsWith(basePath)) return cleaned
|
|
47
52
|
if (baseSegment && cleaned.startsWith(baseSegment)) return `/${cleaned}`
|
|
48
53
|
return `${basePath}${cleaned}`
|
|
49
54
|
}, [src, basePath, baseSegment])
|
|
50
55
|
|
|
56
|
+
const isExternal = /^https?:\/\//.test(rawSrc)
|
|
51
57
|
const scale = zoom / 100
|
|
52
58
|
|
|
53
59
|
const [editing, setEditing] = useState(false)
|
|
@@ -55,6 +61,28 @@ export default forwardRef(function PrototypeEmbed({ props, onUpdate, resizable }
|
|
|
55
61
|
const [expanded, setExpanded] = useState(false)
|
|
56
62
|
const [filter, setFilter] = useState('')
|
|
57
63
|
const [canvasTheme, setCanvasTheme] = useState(() => resolveCanvasThemeFromStorage())
|
|
64
|
+
|
|
65
|
+
// Lazy loading state — only use snapshots that match this widget's ID
|
|
66
|
+
const snapshotMatchesWidget = (url) => url && widgetId && url.includes(widgetId)
|
|
67
|
+
const validSnapshotLight = snapshotMatchesWidget(snapshotLight) ? snapshotLight : null
|
|
68
|
+
const validSnapshotDark = snapshotMatchesWidget(snapshotDark) ? snapshotDark : null
|
|
69
|
+
const currentSnapshot = canvasTheme?.startsWith('dark') ? validSnapshotDark : validSnapshotLight
|
|
70
|
+
const hasSnapshot = !!currentSnapshot
|
|
71
|
+
const [preloadIframe, setPreloadIframe] = useState(!hasSnapshot || isExternal)
|
|
72
|
+
const [iframeLoaded, setIframeLoaded] = useState(false)
|
|
73
|
+
const [showIframe, setShowIframe] = useState(!hasSnapshot || isExternal)
|
|
74
|
+
const [showSpinner, setShowSpinner] = useState(false)
|
|
75
|
+
const capturingRef = useRef(false)
|
|
76
|
+
|
|
77
|
+
// Show spinner only after 500ms of loading
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (showIframe && !iframeLoaded && hasSnapshot) {
|
|
80
|
+
const timer = setTimeout(() => setShowSpinner(true), 500)
|
|
81
|
+
return () => clearTimeout(timer)
|
|
82
|
+
}
|
|
83
|
+
setShowSpinner(false)
|
|
84
|
+
}, [showIframe, iframeLoaded, hasSnapshot])
|
|
85
|
+
|
|
58
86
|
const inputRef = useRef(null)
|
|
59
87
|
const filterRef = useRef(null)
|
|
60
88
|
const embedRef = useRef(null)
|
|
@@ -232,20 +260,100 @@ export default forwardRef(function PrototypeEmbed({ props, onUpdate, resizable }
|
|
|
232
260
|
}
|
|
233
261
|
}, [expanded])
|
|
234
262
|
|
|
235
|
-
// Listen for
|
|
263
|
+
// Listen for messages from the embedded prototype iframe
|
|
236
264
|
useEffect(() => {
|
|
237
265
|
function handleMessage(e) {
|
|
238
|
-
if (
|
|
239
|
-
if (e.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
266
|
+
if (!iframeRef.current?.contentWindow) return
|
|
267
|
+
if (e.source !== iframeRef.current.contentWindow) return
|
|
268
|
+
|
|
269
|
+
// Navigation events
|
|
270
|
+
if (e.data?.type === 'storyboard:embed:navigate') {
|
|
271
|
+
const newSrc = e.data.src
|
|
272
|
+
if (newSrc && newSrc !== src) {
|
|
273
|
+
const originalSrc = readProp(props, 'originalSrc', prototypeEmbedSchema)
|
|
274
|
+
onUpdate?.({ src: newSrc, originalSrc: originalSrc || src })
|
|
275
|
+
}
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Snapshot capture responses
|
|
280
|
+
if (e.data?.type === 'storyboard:embed:snapshot') {
|
|
281
|
+
if (e.data.error) {
|
|
282
|
+
console.warn('[canvas] Snapshot capture failed:', e.data.error)
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
handleSnapshotResult(e.data.requestId, e.data.dataUrl)
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Snapshot-ready signal — iframe content has fully rendered
|
|
290
|
+
if (e.data?.type === 'storyboard:embed:snapshot-ready') {
|
|
291
|
+
setIframeLoaded(true)
|
|
292
|
+
if (onUpdate && !isExternal) requestSnapshotCapture()
|
|
244
293
|
}
|
|
245
294
|
}
|
|
246
295
|
window.addEventListener('message', handleMessage)
|
|
247
296
|
return () => window.removeEventListener('message', handleMessage)
|
|
248
|
-
}, [src, props, onUpdate])
|
|
297
|
+
}, [src, props, onUpdate, isExternal])
|
|
298
|
+
|
|
299
|
+
// Request a snapshot capture from the iframe
|
|
300
|
+
const requestSnapshotCapture = useCallback(() => {
|
|
301
|
+
if (!iframeRef.current?.contentWindow || capturingRef.current || isExternal) return
|
|
302
|
+
capturingRef.current = true
|
|
303
|
+
const requestId = `snap-${Date.now()}`
|
|
304
|
+
iframeRef.current.contentWindow.postMessage({
|
|
305
|
+
type: 'storyboard:embed:capture',
|
|
306
|
+
requestId,
|
|
307
|
+
}, '*')
|
|
308
|
+
}, [isExternal])
|
|
309
|
+
|
|
310
|
+
// Handle a completed snapshot — upload and persist as widget prop
|
|
311
|
+
const handleSnapshotResult = useCallback(async (requestId, dataUrl) => {
|
|
312
|
+
if (!dataUrl || !onUpdate || !widgetId) return
|
|
313
|
+
capturingRef.current = false
|
|
314
|
+
try {
|
|
315
|
+
const result = await uploadImage(dataUrl, `snapshot-${widgetId}`)
|
|
316
|
+
if (!result?.success || !result?.filename) return
|
|
317
|
+
const imageUrl = `/_storyboard/canvas/images/${result.filename}`
|
|
318
|
+
const themeKey = canvasTheme?.startsWith('dark') ? 'snapshotDark' : 'snapshotLight'
|
|
319
|
+
onUpdate?.({ [themeKey]: imageUrl })
|
|
320
|
+
} catch (err) {
|
|
321
|
+
console.warn('[canvas] Failed to upload snapshot:', err)
|
|
322
|
+
}
|
|
323
|
+
}, [onUpdate, canvasTheme, widgetId])
|
|
324
|
+
|
|
325
|
+
// Re-capture snapshots after resize (debounced)
|
|
326
|
+
const resizeCaptureTimer = useRef(null)
|
|
327
|
+
const triggerResizeCapture = useCallback(() => {
|
|
328
|
+
if (!onUpdate || isExternal) return
|
|
329
|
+
clearTimeout(resizeCaptureTimer.current)
|
|
330
|
+
resizeCaptureTimer.current = setTimeout(() => {
|
|
331
|
+
requestSnapshotCapture()
|
|
332
|
+
}, 2000)
|
|
333
|
+
}, [requestSnapshotCapture, isExternal, onUpdate])
|
|
334
|
+
|
|
335
|
+
// Re-capture when src changes (new prototype selected)
|
|
336
|
+
const prevSrcRef = useRef(src)
|
|
337
|
+
useEffect(() => {
|
|
338
|
+
if (src && src !== prevSrcRef.current && onUpdate && !isExternal && showIframe) {
|
|
339
|
+
prevSrcRef.current = src
|
|
340
|
+
// Wait for the new page to render
|
|
341
|
+
const timer = setTimeout(() => requestSnapshotCapture(), 4000)
|
|
342
|
+
return () => clearTimeout(timer)
|
|
343
|
+
}
|
|
344
|
+
prevSrcRef.current = src
|
|
345
|
+
}, [src, onUpdate, isExternal, showIframe, requestSnapshotCapture])
|
|
346
|
+
|
|
347
|
+
// Re-capture for the alternate theme variant when theme changes
|
|
348
|
+
const prevThemeRef = useRef(canvasTheme)
|
|
349
|
+
useEffect(() => {
|
|
350
|
+
if (canvasTheme !== prevThemeRef.current && onUpdate && !isExternal && showIframe) {
|
|
351
|
+
prevThemeRef.current = canvasTheme
|
|
352
|
+
const timer = setTimeout(() => requestSnapshotCapture(), 3000)
|
|
353
|
+
return () => clearTimeout(timer)
|
|
354
|
+
}
|
|
355
|
+
prevThemeRef.current = canvasTheme
|
|
356
|
+
}, [canvasTheme, onUpdate, isExternal, showIframe, requestSnapshotCapture])
|
|
249
357
|
|
|
250
358
|
const chromeVars = useMemo(() => getEmbedChromeVars(canvasTheme), [canvasTheme])
|
|
251
359
|
|
|
@@ -260,15 +368,9 @@ export default forwardRef(function PrototypeEmbed({ props, onUpdate, resizable }
|
|
|
260
368
|
setExpanded(true)
|
|
261
369
|
} else if (actionId === 'open-external') {
|
|
262
370
|
if (rawSrc) window.open(rawSrc, '_blank', 'noopener')
|
|
263
|
-
} else if (actionId === 'zoom-in') {
|
|
264
|
-
const step = zoom < 75 ? 5 : 25
|
|
265
|
-
onUpdate?.({ zoom: Math.min(200, zoom + step) })
|
|
266
|
-
} else if (actionId === 'zoom-out') {
|
|
267
|
-
const step = zoom <= 75 ? 5 : 25
|
|
268
|
-
onUpdate?.({ zoom: Math.max(25, zoom - step) })
|
|
269
371
|
}
|
|
270
372
|
},
|
|
271
|
-
}), [rawSrc
|
|
373
|
+
}), [rawSrc])
|
|
272
374
|
|
|
273
375
|
function handlePickRoute(route) {
|
|
274
376
|
onUpdate?.({ src: route })
|
|
@@ -380,30 +482,78 @@ export default forwardRef(function PrototypeEmbed({ props, onUpdate, resizable }
|
|
|
380
482
|
</div>
|
|
381
483
|
) : iframeSrc ? (
|
|
382
484
|
<>
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
className={styles.iframeContainer}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
485
|
+
{/* Snapshot image — shown until iframe is fully loaded */}
|
|
486
|
+
{hasSnapshot && !(showIframe && iframeLoaded) && (
|
|
487
|
+
<div className={styles.iframeContainer}>
|
|
488
|
+
<img
|
|
489
|
+
src={basePath + currentSnapshot}
|
|
490
|
+
alt={label || 'Prototype preview'}
|
|
491
|
+
className={styles.snapshotImage}
|
|
492
|
+
style={{ width, height }}
|
|
493
|
+
draggable={false}
|
|
494
|
+
/>
|
|
495
|
+
{showIframe && !iframeLoaded && showSpinner && (
|
|
496
|
+
<div className={styles.snapshotSpinner}>
|
|
497
|
+
<div className={styles.spinner} />
|
|
498
|
+
</div>
|
|
499
|
+
)}
|
|
500
|
+
</div>
|
|
501
|
+
)}
|
|
502
|
+
|
|
503
|
+
{/* Iframe — preloaded on hover, revealed after load */}
|
|
504
|
+
{(preloadIframe || showIframe) && (
|
|
505
|
+
<div
|
|
506
|
+
ref={inlineContainerRef}
|
|
507
|
+
className={styles.iframeContainer}
|
|
508
|
+
style={
|
|
509
|
+
expanded ? { visibility: 'hidden' }
|
|
510
|
+
: (hasSnapshot && !(showIframe && iframeLoaded)) ? { position: 'absolute', top: 0, left: 0, opacity: 0, pointerEvents: 'none' }
|
|
511
|
+
: undefined
|
|
512
|
+
}
|
|
513
|
+
>
|
|
514
|
+
<iframe
|
|
515
|
+
ref={iframeRef}
|
|
516
|
+
src={iframeSrc}
|
|
517
|
+
className={styles.iframe}
|
|
518
|
+
style={{
|
|
519
|
+
width: width / scale,
|
|
520
|
+
height: height / scale,
|
|
521
|
+
transform: `scale(${scale})`,
|
|
522
|
+
transformOrigin: '0 0',
|
|
523
|
+
}}
|
|
524
|
+
title={label || 'Prototype embed'}
|
|
525
|
+
sandbox="allow-same-origin allow-scripts allow-forms allow-popups"
|
|
526
|
+
/>
|
|
527
|
+
</div>
|
|
528
|
+
)}
|
|
529
|
+
|
|
402
530
|
{!interactive && !expanded && (
|
|
403
531
|
<div
|
|
404
|
-
className={
|
|
405
|
-
|
|
406
|
-
|
|
532
|
+
className={overlayStyles.interactOverlay}
|
|
533
|
+
onPointerEnter={() => {
|
|
534
|
+
if (!preloadIframe) setPreloadIframe(true)
|
|
535
|
+
}}
|
|
536
|
+
onClick={(e) => {
|
|
537
|
+
if (e.shiftKey || e.metaKey || e.ctrlKey || e.altKey) return
|
|
538
|
+
setShowIframe(true)
|
|
539
|
+
setPreloadIframe(true)
|
|
540
|
+
enterInteractive()
|
|
541
|
+
}}
|
|
542
|
+
role="button"
|
|
543
|
+
tabIndex={0}
|
|
544
|
+
onKeyDown={(e) => {
|
|
545
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
546
|
+
e.preventDefault()
|
|
547
|
+
e.stopPropagation()
|
|
548
|
+
setShowIframe(true)
|
|
549
|
+
setPreloadIframe(true)
|
|
550
|
+
enterInteractive()
|
|
551
|
+
}
|
|
552
|
+
}}
|
|
553
|
+
aria-label="Click to interact with prototype"
|
|
554
|
+
>
|
|
555
|
+
<span className={overlayStyles.interactHint}>Click to interact</span>
|
|
556
|
+
</div>
|
|
407
557
|
)}
|
|
408
558
|
</>
|
|
409
559
|
) : (
|
|
@@ -436,6 +586,7 @@ export default forwardRef(function PrototypeEmbed({ props, onUpdate, resizable }
|
|
|
436
586
|
function onUp() {
|
|
437
587
|
document.removeEventListener('mousemove', onMove)
|
|
438
588
|
document.removeEventListener('mouseup', onUp)
|
|
589
|
+
triggerResizeCapture()
|
|
439
590
|
}
|
|
440
591
|
document.addEventListener('mousemove', onMove)
|
|
441
592
|
document.addEventListener('mouseup', onUp)
|
|
@@ -450,8 +601,13 @@ export default forwardRef(function PrototypeEmbed({ props, onUpdate, resizable }
|
|
|
450
601
|
style={expanded ? undefined : { display: 'none' }}
|
|
451
602
|
onClick={() => setExpanded(false)}
|
|
452
603
|
onPointerDown={(e) => e.stopPropagation()}
|
|
453
|
-
onKeyDown={(e) =>
|
|
604
|
+
onKeyDown={(e) => {
|
|
605
|
+
e.stopPropagation()
|
|
606
|
+
if (e.key === 'Escape') setExpanded(false)
|
|
607
|
+
}}
|
|
454
608
|
onWheel={(e) => e.stopPropagation()}
|
|
609
|
+
tabIndex={-1}
|
|
610
|
+
ref={(el) => { if (el && expanded) el.focus() }}
|
|
455
611
|
>
|
|
456
612
|
<div
|
|
457
613
|
ref={modalContainerRef}
|
|
@@ -18,11 +18,38 @@
|
|
|
18
18
|
display: block;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
.
|
|
21
|
+
.snapshotImage {
|
|
22
|
+
display: block;
|
|
23
|
+
object-fit: cover;
|
|
24
|
+
object-position: top left;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.snapshotSpinner {
|
|
22
28
|
position: absolute;
|
|
23
29
|
inset: 0;
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: center;
|
|
33
|
+
background: rgba(0, 0, 0, 0.08);
|
|
34
|
+
animation: fadeIn 150ms ease;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.spinner {
|
|
38
|
+
width: 24px;
|
|
39
|
+
height: 24px;
|
|
40
|
+
border: 2.5px solid var(--borderColor-default, #d0d7de);
|
|
41
|
+
border-top-color: var(--fgColor-accent, #0969da);
|
|
42
|
+
border-radius: 50%;
|
|
43
|
+
animation: spin 0.6s linear infinite;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@keyframes spin {
|
|
47
|
+
to { transform: rotate(360deg); }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@keyframes fadeIn {
|
|
51
|
+
from { opacity: 0; }
|
|
52
|
+
to { opacity: 1; }
|
|
26
53
|
}
|
|
27
54
|
|
|
28
55
|
.empty {
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
box-shadow: 2px 3px 10px rgba(0, 0, 0, 0.35);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/* Hide own border when parent widget slot is selected (avoid double focus ring) */
|
|
24
|
+
:global([data-widget-selected]) .sticky {
|
|
25
|
+
border-color: transparent;
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
.text {
|
|
24
29
|
padding: 16px 20px;
|
|
25
30
|
margin: 0;
|
|
@@ -13,16 +13,16 @@ describe('stickyNoteSchema', () => {
|
|
|
13
13
|
)
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
-
it('
|
|
16
|
+
it('includes default values for width/height from config', () => {
|
|
17
17
|
const defaults = getDefaults(stickyNoteSchema)
|
|
18
|
-
expect(defaults).
|
|
19
|
-
expect(defaults).
|
|
18
|
+
expect(defaults).toHaveProperty('width', 270)
|
|
19
|
+
expect(defaults).toHaveProperty('height', 170)
|
|
20
20
|
})
|
|
21
21
|
|
|
22
|
-
it('returns
|
|
22
|
+
it('returns default value when width/height are not saved in props', () => {
|
|
23
23
|
const props = { text: 'hello', color: 'yellow' }
|
|
24
|
-
expect(readProp(props, 'width', stickyNoteSchema)).
|
|
25
|
-
expect(readProp(props, 'height', stickyNoteSchema)).
|
|
24
|
+
expect(readProp(props, 'width', stickyNoteSchema)).toBe(270)
|
|
25
|
+
expect(readProp(props, 'height', stickyNoteSchema)).toBe(170)
|
|
26
26
|
})
|
|
27
27
|
|
|
28
28
|
it('returns saved width/height when present in props', () => {
|
|
@@ -33,11 +33,11 @@ describe('stickyNoteSchema', () => {
|
|
|
33
33
|
})
|
|
34
34
|
|
|
35
35
|
describe('StickyNote', () => {
|
|
36
|
-
it('
|
|
36
|
+
it('applies default dimensions as inline styles when not saved in props', () => {
|
|
37
37
|
const { container } = render(<StickyNote props={{ text: 'Hi' }} onUpdate={vi.fn()} />)
|
|
38
38
|
const sticky = container.querySelector('article')
|
|
39
|
-
expect(sticky.style.width).toBe('')
|
|
40
|
-
expect(sticky.style.height).toBe('')
|
|
39
|
+
expect(sticky.style.width).toBe('270px')
|
|
40
|
+
expect(sticky.style.height).toBe('170px')
|
|
41
41
|
})
|
|
42
42
|
|
|
43
43
|
it('applies saved dimensions as inline styles', () => {
|