@cdc/markup-include 4.26.2 → 4.26.4
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/CONFIG.md +113 -0
- package/README.md +38 -14
- package/dist/cdcmarkupinclude.js +20835 -12893
- package/examples/minimal-example.json +11 -0
- package/package.json +4 -4
- package/src/CdcMarkupInclude.tsx +192 -108
- package/src/ConfigContext.ts +1 -0
- package/src/_stories/MarkupInclude.Editor.stories.tsx +57 -17
- package/src/_stories/MarkupInclude.smoke.stories.tsx +33 -0
- package/src/_stories/MarkupInclude.stories.tsx +81 -0
- package/src/cdcMarkupInclude.style.css +15 -11
- package/src/components/EditorPanel/EditorPanel.styles.css +1 -1
- package/src/components/EditorPanel/EditorPanel.tsx +271 -16
- package/src/data/initial-state.js +7 -1
- package/src/scss/main.scss +81 -10
- package/src/test/CdcMarkupInclude.test.jsx +74 -3
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cdc/markup-include",
|
|
3
|
-
"version": "4.26.
|
|
3
|
+
"version": "4.26.4",
|
|
4
4
|
"description": "React component for displaying HTML content from an outside link",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Rob Shelnutt <rob@blackairplane.com>",
|
|
7
7
|
"bugs": "https://github.com/CDCgov/cdc-open-viz/issues",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@cdc/core": "^4.26.
|
|
9
|
+
"@cdc/core": "^4.26.4",
|
|
10
10
|
"axios": "^1.13.2",
|
|
11
|
-
"
|
|
11
|
+
"dompurify": "^3.4.0",
|
|
12
12
|
"lodash": "^4.17.23",
|
|
13
13
|
"react-accessible-accordion": "^5.0.1"
|
|
14
14
|
},
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"vite-plugin-css-injected-by-js": "^2.4.0",
|
|
20
20
|
"vite-plugin-svgr": "^4.2.0"
|
|
21
21
|
},
|
|
22
|
-
"gitHead": "
|
|
22
|
+
"gitHead": "6097de1ff814001880d9ac64bd66becdc092d63c",
|
|
23
23
|
"homepage": "https://github.com/CDCgov/cdc-open-viz#readme",
|
|
24
24
|
"main": "dist/cdcmarkupinclude",
|
|
25
25
|
"moduleName": "CdcMarkupInclude",
|
package/src/CdcMarkupInclude.tsx
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { useEffect, useCallback, useRef, useReducer, useMemo } from 'react'
|
|
2
|
-
import _ from 'lodash'
|
|
3
2
|
// external
|
|
4
|
-
import
|
|
3
|
+
import DOMPurify from 'dompurify'
|
|
5
4
|
import axios from 'axios'
|
|
5
|
+
import parse from 'html-react-parser'
|
|
6
6
|
|
|
7
7
|
// cdc
|
|
8
8
|
import { MarkupIncludeConfig } from '@cdc/core/types/MarkupInclude'
|
|
9
9
|
import { publish } from '@cdc/core/helpers/events'
|
|
10
10
|
import { processMarkupVariables } from '@cdc/core/helpers/markupProcessor'
|
|
11
11
|
import { addValuesToFilters } from '@cdc/core/helpers/addValuesToFilters'
|
|
12
|
+
import { resolveDataColor } from '@cdc/core/helpers/dataColors'
|
|
12
13
|
import ConfigContext from './ConfigContext'
|
|
13
14
|
import coveUpdateWorker from '@cdc/core/helpers/coveUpdateWorker'
|
|
15
|
+
import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
|
|
14
16
|
import EditorPanel from '../src/components/EditorPanel'
|
|
15
17
|
import defaults from './data/initial-state'
|
|
16
18
|
|
|
@@ -19,7 +21,7 @@ import Loading from '@cdc/core/components/Loading'
|
|
|
19
21
|
import Filters from '@cdc/core/components/Filters'
|
|
20
22
|
import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses'
|
|
21
23
|
import markupIncludeReducer from './store/markupInclude.reducer'
|
|
22
|
-
import
|
|
24
|
+
import { VisualizationContainer, VisualizationContent } from '@cdc/core/components/Layout'
|
|
23
25
|
// styles
|
|
24
26
|
import './cdcMarkupInclude.style.css'
|
|
25
27
|
import './scss/main.scss'
|
|
@@ -30,6 +32,7 @@ type CdcMarkupIncludeProps = {
|
|
|
30
32
|
datasets: Datasets
|
|
31
33
|
isDashboard: boolean
|
|
32
34
|
isEditor: boolean
|
|
35
|
+
rawData?: any[]
|
|
33
36
|
setConfig: any
|
|
34
37
|
interactionLabel?: string
|
|
35
38
|
}
|
|
@@ -45,6 +48,7 @@ const CdcMarkupInclude: React.FC<CdcMarkupIncludeProps> = ({
|
|
|
45
48
|
datasets,
|
|
46
49
|
isDashboard = true,
|
|
47
50
|
isEditor = false,
|
|
51
|
+
rawData,
|
|
48
52
|
setConfig: setParentConfig,
|
|
49
53
|
interactionLabel = 'no link provided'
|
|
50
54
|
}) => {
|
|
@@ -64,7 +68,7 @@ const CdcMarkupInclude: React.FC<CdcMarkupIncludeProps> = ({
|
|
|
64
68
|
// Custom States
|
|
65
69
|
const container = useRef()
|
|
66
70
|
|
|
67
|
-
const { innerContainerClasses, contentClasses } = useDataVizClasses(config || {})
|
|
71
|
+
const { innerContainerClasses, contentClasses: rawContentClasses } = useDataVizClasses(config || {})
|
|
68
72
|
const { contentEditor, theme, visual } = config || {}
|
|
69
73
|
const {
|
|
70
74
|
showNoDataMessage,
|
|
@@ -76,13 +80,45 @@ const CdcMarkupInclude: React.FC<CdcMarkupIncludeProps> = ({
|
|
|
76
80
|
|
|
77
81
|
// Support markupVariables at root level or inside contentEditor
|
|
78
82
|
const markupVariables = config?.markupVariables || contentEditorMarkupVariables || []
|
|
83
|
+
const editorData = useMemo(() => {
|
|
84
|
+
if (isDashboard && isEditor && Array.isArray(rawData) && rawData.length) {
|
|
85
|
+
return rawData
|
|
86
|
+
}
|
|
79
87
|
|
|
80
|
-
|
|
88
|
+
const assignedDatasetData = config?.dataKey ? datasets?.[config.dataKey]?.data : undefined
|
|
89
|
+
if (Array.isArray(assignedDatasetData) && assignedDatasetData.length) {
|
|
90
|
+
return assignedDatasetData
|
|
91
|
+
}
|
|
81
92
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
return data || []
|
|
94
|
+
}, [config?.dataKey, data, datasets, isDashboard, isEditor, rawData])
|
|
95
|
+
|
|
96
|
+
const { inlineHTML, srcUrl, title, useInlineHTML, style: contentStyle } = contentEditor || {}
|
|
97
|
+
const markupIncludeStyle = contentStyle || 'default'
|
|
98
|
+
const isTp5Style = markupIncludeStyle === 'tp5'
|
|
99
|
+
|
|
100
|
+
const dataColorResolution = useMemo(() => {
|
|
101
|
+
const dataArr = Array.isArray(data) ? data : []
|
|
102
|
+
return resolveDataColor({
|
|
103
|
+
data: dataArr,
|
|
104
|
+
dataColors: config?.dataColors
|
|
105
|
+
})
|
|
106
|
+
}, [data, config?.dataColors])
|
|
107
|
+
|
|
108
|
+
const contentClasses = isTp5Style
|
|
109
|
+
? rawContentClasses.filter(
|
|
110
|
+
cls =>
|
|
111
|
+
cls !== 'component--has-accent' &&
|
|
112
|
+
cls !== 'component--has-background' &&
|
|
113
|
+
cls !== 'component--hide-background-color' &&
|
|
114
|
+
cls !== 'component--has-border-color-theme'
|
|
115
|
+
)
|
|
116
|
+
: rawContentClasses
|
|
85
117
|
|
|
118
|
+
const shouldApplyTopPadding =
|
|
119
|
+
!isTp5Style &&
|
|
120
|
+
(visual?.border || visual?.background || (contentEditor?.title && contentEditor?.titleStyle === 'legacy'))
|
|
121
|
+
const shouldApplySidePadding = !isTp5Style && (visual?.border || visual?.accent || visual?.background)
|
|
86
122
|
// Default Functions
|
|
87
123
|
const updateConfig = newConfig => {
|
|
88
124
|
Object.keys(defaults).forEach(key => {
|
|
@@ -112,8 +148,9 @@ const CdcMarkupInclude: React.FC<CdcMarkupIncludeProps> = ({
|
|
|
112
148
|
let responseData = response.data ?? {}
|
|
113
149
|
|
|
114
150
|
if (response.dataUrl) {
|
|
115
|
-
const
|
|
116
|
-
responseData =
|
|
151
|
+
const { data, dataMetadata } = await fetchRemoteData(response.dataUrl)
|
|
152
|
+
responseData = data
|
|
153
|
+
response.dataMetadata = dataMetadata
|
|
117
154
|
}
|
|
118
155
|
|
|
119
156
|
response.data = responseData
|
|
@@ -203,63 +240,29 @@ const CdcMarkupInclude: React.FC<CdcMarkupIncludeProps> = ({
|
|
|
203
240
|
}
|
|
204
241
|
|
|
205
242
|
/**
|
|
206
|
-
*
|
|
207
|
-
* This ensures that the CSS is applied only to this COVE visualization.
|
|
243
|
+
* Extracts <style> tags from HTML and scopes them using CSS nesting under the given scope ID.
|
|
208
244
|
*/
|
|
209
|
-
const
|
|
210
|
-
if (!html || typeof html !== 'string') return html
|
|
245
|
+
const extractAndScopeStyles = (html: string, scopeId: string): { scopedCSS: string; cleanHTML: string } => {
|
|
246
|
+
if (!html || typeof html !== 'string') return { scopedCSS: '', cleanHTML: html }
|
|
211
247
|
|
|
212
|
-
// Use DOMParser to parse HTML
|
|
213
248
|
const parser = new DOMParser()
|
|
214
249
|
const doc = parser.parseFromString(html, 'text/html')
|
|
215
250
|
|
|
216
|
-
// Extract all <style> elements
|
|
217
251
|
const styleElements = doc.querySelectorAll('style')
|
|
218
|
-
if (styleElements.length === 0) return html
|
|
219
|
-
|
|
220
|
-
// Parse CSS rules
|
|
221
|
-
const sheet = new CSSStyleSheet()
|
|
222
|
-
const cssRules: Array<{ selector: string; styles: string }> = []
|
|
252
|
+
if (styleElements.length === 0) return { scopedCSS: '', cleanHTML: html }
|
|
223
253
|
|
|
254
|
+
const cssFragments: string[] = []
|
|
224
255
|
styleElements.forEach(styleEl => {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
// Extract parsed rules from the stylesheet
|
|
230
|
-
for (let i = 0; i < sheet.cssRules.length; i++) {
|
|
231
|
-
const rule = sheet.cssRules[i]
|
|
232
|
-
if (rule instanceof CSSStyleRule) {
|
|
233
|
-
cssRules.push({
|
|
234
|
-
selector: rule.selectorText,
|
|
235
|
-
styles: rule.style.cssText
|
|
236
|
-
})
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
} catch (e) {
|
|
240
|
-
console.warn('Markup Include: Invalid CSS in style tag', e)
|
|
256
|
+
const text = styleEl.textContent?.trim()
|
|
257
|
+
if (text) {
|
|
258
|
+
cssFragments.push(text)
|
|
241
259
|
}
|
|
242
|
-
|
|
243
260
|
styleEl.remove()
|
|
244
261
|
})
|
|
245
262
|
|
|
246
|
-
|
|
247
|
-
for (const rule of cssRules) {
|
|
248
|
-
try {
|
|
249
|
-
const elements = doc.body.querySelectorAll(rule.selector)
|
|
250
|
-
|
|
251
|
-
elements.forEach(el => {
|
|
252
|
-
const existingStyle = el.getAttribute('style') || ''
|
|
253
|
-
const newStyle = existingStyle ? `${existingStyle}; ${rule.styles}` : rule.styles
|
|
254
|
-
el.setAttribute('style', newStyle)
|
|
255
|
-
})
|
|
256
|
-
} catch (e) {
|
|
257
|
-
// Skip invalid selectors (e.g., pseudo-selectors like :hover won't match)
|
|
258
|
-
console.warn(`Markup Include: Could not apply CSS rule for selector "${rule.selector}"`, e)
|
|
259
|
-
}
|
|
260
|
-
}
|
|
263
|
+
const scopedCSS = cssFragments.length > 0 ? `#${scopeId} {\n${cssFragments.join('\n')}\n}` : ''
|
|
261
264
|
|
|
262
|
-
return doc.body.innerHTML
|
|
265
|
+
return { scopedCSS, cleanHTML: doc.body.innerHTML }
|
|
263
266
|
}
|
|
264
267
|
|
|
265
268
|
//Load initial config
|
|
@@ -289,69 +292,144 @@ const CdcMarkupInclude: React.FC<CdcMarkupIncludeProps> = ({
|
|
|
289
292
|
allowHideSection,
|
|
290
293
|
filters: config?.filters || [],
|
|
291
294
|
datasets,
|
|
292
|
-
configDataKey: config?.dataKey
|
|
295
|
+
configDataKey: config?.dataKey,
|
|
296
|
+
locale: config?.locale,
|
|
297
|
+
dataMetadata: config?.dataMetadata
|
|
293
298
|
})
|
|
294
299
|
: { processedContent: parseBodyMarkup(urlMarkup), shouldHideSection: false, shouldShowNoDataMessage: false }
|
|
295
300
|
|
|
296
|
-
const
|
|
301
|
+
const scopeId = `cove-mi-${config?.runtime?.uniqueId || 'default'}`
|
|
302
|
+
const { scopedCSS, cleanHTML } = extractAndScopeStyles(processedMarkup.processedContent, scopeId)
|
|
303
|
+
const sanitizedHTML = cleanHTML ? DOMPurify.sanitize(cleanHTML) : ''
|
|
297
304
|
|
|
298
305
|
const hideMarkupInclude = processedMarkup.shouldHideSection
|
|
299
306
|
const _showNoDataMessage = processedMarkup.shouldShowNoDataMessage
|
|
300
307
|
|
|
308
|
+
const processedTitle = useMemo(() => {
|
|
309
|
+
if (!config?.enableMarkupVariables || !markupVariables?.length || !title) return title
|
|
310
|
+
return processMarkupVariables(title, data || [], markupVariables, {
|
|
311
|
+
isEditor,
|
|
312
|
+
showNoDataMessage,
|
|
313
|
+
allowHideSection,
|
|
314
|
+
filters: config?.filters || [],
|
|
315
|
+
datasets,
|
|
316
|
+
configDataKey: config?.dataKey,
|
|
317
|
+
locale: config?.locale,
|
|
318
|
+
dataMetadata: config?.dataMetadata
|
|
319
|
+
}).processedContent
|
|
320
|
+
}, [
|
|
321
|
+
title,
|
|
322
|
+
data,
|
|
323
|
+
markupVariables,
|
|
324
|
+
config?.enableMarkupVariables,
|
|
325
|
+
config?.filters,
|
|
326
|
+
config?.dataKey,
|
|
327
|
+
config?.locale,
|
|
328
|
+
config?.dataMetadata,
|
|
329
|
+
isEditor,
|
|
330
|
+
showNoDataMessage,
|
|
331
|
+
allowHideSection,
|
|
332
|
+
datasets
|
|
333
|
+
])
|
|
334
|
+
|
|
301
335
|
if (loading === false) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
{
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
336
|
+
const hasTp5Title = processedTitle && processedTitle.trim()
|
|
337
|
+
content = !hideMarkupInclude && (
|
|
338
|
+
<VisualizationContent
|
|
339
|
+
innerClassName={`markup-include-content-container ${innerContainerClasses.join(' ')}`.trim()}
|
|
340
|
+
bodyClassName={`markup-include-component ${contentClasses.join(' ')}${
|
|
341
|
+
isTp5Style ? ' markup-include-component--tp5' : ''
|
|
342
|
+
}${isTp5Style && visual?.whiteBackground ? ' white-background-style' : ''}${
|
|
343
|
+
isTp5Style && visual?.whiteBackground && visual?.border ? ' display-border' : ''
|
|
344
|
+
}`.trim()}
|
|
345
|
+
bodyWrapClassName={`${isTp5Style ? 'markup-include-body-wrap--tp5' : ''}${
|
|
346
|
+
shouldApplyTopPadding ? ' has-top-padding' : ''
|
|
347
|
+
}${shouldApplySidePadding ? ' has-side-padding' : ''}`.trim()}
|
|
348
|
+
filters={
|
|
349
|
+
config.filters && config.filters.length > 0 ? (
|
|
350
|
+
<Filters
|
|
351
|
+
config={config}
|
|
352
|
+
setFilters={setFilters}
|
|
353
|
+
excludedData={data || []}
|
|
354
|
+
dimensions={[0, 0]}
|
|
355
|
+
interactionLabel={interactionLabel || 'markup-include'}
|
|
356
|
+
/>
|
|
357
|
+
) : null
|
|
358
|
+
}
|
|
359
|
+
header={
|
|
360
|
+
!isTp5Style ? (
|
|
361
|
+
<Title
|
|
362
|
+
title={processedTitle}
|
|
363
|
+
isDashboard={isDashboard}
|
|
364
|
+
titleStyle={contentEditor?.titleStyle}
|
|
365
|
+
config={config}
|
|
366
|
+
classes={[`${theme}`, 'mb-0']}
|
|
367
|
+
noContent={!sanitizedHTML}
|
|
368
|
+
/>
|
|
369
|
+
) : null
|
|
370
|
+
}
|
|
371
|
+
footer={
|
|
372
|
+
<FootnotesStandAlone
|
|
373
|
+
config={config?.footnotes}
|
|
374
|
+
filters={config?.filters || []}
|
|
375
|
+
markupVariables={markupVariables}
|
|
376
|
+
enableMarkupVariables={config?.enableMarkupVariables}
|
|
377
|
+
data={data}
|
|
378
|
+
dataMetadata={config?.dataMetadata}
|
|
379
|
+
footerClassName={isTp5Style ? 'mt-3' : undefined}
|
|
380
|
+
/>
|
|
381
|
+
}
|
|
382
|
+
>
|
|
383
|
+
{isTp5Style ? (
|
|
384
|
+
<div
|
|
385
|
+
className={`markup-include-tp5 cdc-callout d-flex flex-column h-100 ${
|
|
386
|
+
dataColorResolution.state === 'resolved' ? 'cdc-callout--data-color' : ''
|
|
387
|
+
}`}
|
|
388
|
+
style={
|
|
389
|
+
dataColorResolution.state === 'resolved'
|
|
390
|
+
? { backgroundColor: dataColorResolution.color, color: dataColorResolution.textColor }
|
|
391
|
+
: undefined
|
|
392
|
+
}
|
|
393
|
+
>
|
|
394
|
+
{hasTp5Title && (
|
|
395
|
+
<h3 className='cdc-callout__heading cove-prose fw-bold flex-shrink-0 d-flex align-items-start'>
|
|
396
|
+
<span>{parse(processedTitle.trim())}</span>
|
|
397
|
+
</h3>
|
|
398
|
+
)}
|
|
399
|
+
<div className='cdc-callout__body d-flex flex-row align-content-start flex-grow-1'>
|
|
400
|
+
<div className='cdc-callout__content flex-grow-1 d-flex flex-column min-w-0'>
|
|
401
|
+
{_showNoDataMessage && (
|
|
402
|
+
<div className='no-data-message'>
|
|
403
|
+
<p>{`${noDataMessageText}`}</p>
|
|
404
|
+
</div>
|
|
405
|
+
)}
|
|
406
|
+
{!markupError && !_showNoDataMessage && (
|
|
407
|
+
<div id={scopeId}>
|
|
408
|
+
{scopedCSS && <style>{scopedCSS}</style>}
|
|
409
|
+
<div className='cove-prose' dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />
|
|
341
410
|
</div>
|
|
342
|
-
|
|
411
|
+
)}
|
|
412
|
+
{markupError && srcUrl && !_showNoDataMessage && <div className='warning'>{errorMessage}</div>}
|
|
343
413
|
</div>
|
|
344
|
-
<FootnotesStandAlone
|
|
345
|
-
config={configObj?.footnotes}
|
|
346
|
-
filters={config?.filters || []}
|
|
347
|
-
markupVariables={markupVariables}
|
|
348
|
-
enableMarkupVariables={config?.enableMarkupVariables}
|
|
349
|
-
data={data}
|
|
350
|
-
/>
|
|
351
414
|
</div>
|
|
352
|
-
</
|
|
415
|
+
</div>
|
|
416
|
+
) : (
|
|
417
|
+
<>
|
|
418
|
+
{_showNoDataMessage && (
|
|
419
|
+
<div className='no-data-message'>
|
|
420
|
+
<p>{`${noDataMessageText}`}</p>
|
|
421
|
+
</div>
|
|
422
|
+
)}
|
|
423
|
+
{!markupError && !_showNoDataMessage && (
|
|
424
|
+
<div id={scopeId}>
|
|
425
|
+
{scopedCSS && <style>{scopedCSS}</style>}
|
|
426
|
+
<div className='cove-prose' dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />
|
|
427
|
+
</div>
|
|
428
|
+
)}
|
|
429
|
+
{markupError && srcUrl && !_showNoDataMessage && <div className='warning'>{errorMessage}</div>}
|
|
430
|
+
</>
|
|
353
431
|
)}
|
|
354
|
-
|
|
432
|
+
</VisualizationContent>
|
|
355
433
|
)
|
|
356
434
|
}
|
|
357
435
|
|
|
@@ -368,11 +446,17 @@ const CdcMarkupInclude: React.FC<CdcMarkupIncludeProps> = ({
|
|
|
368
446
|
|
|
369
447
|
return (
|
|
370
448
|
<ErrorBoundary component='CdcMarkupInclude'>
|
|
371
|
-
<ConfigContext.Provider
|
|
449
|
+
<ConfigContext.Provider
|
|
450
|
+
value={{ config, updateConfig, loading, data: data, editorData, setParentConfig, isDashboard }}
|
|
451
|
+
>
|
|
372
452
|
{!config?.newViz && config?.runtime && config?.runtime.editorErrorMessage && <Error />}
|
|
373
|
-
<
|
|
453
|
+
<VisualizationContainer
|
|
454
|
+
config={config as any}
|
|
455
|
+
isEditor={isEditor}
|
|
456
|
+
editorPanel={<EditorPanel datasets={datasets} />}
|
|
457
|
+
>
|
|
374
458
|
{content}
|
|
375
|
-
</
|
|
459
|
+
</VisualizationContainer>
|
|
376
460
|
</ConfigContext.Provider>
|
|
377
461
|
</ErrorBoundary>
|
|
378
462
|
)
|
package/src/ConfigContext.ts
CHANGED
|
@@ -75,8 +75,10 @@ const testConfig = {
|
|
|
75
75
|
contentEditor: {
|
|
76
76
|
inlineHTML: '<h2>Test Markup Include</h2><p>{{test_variable}}</p>',
|
|
77
77
|
showHeader: true,
|
|
78
|
+
style: 'default',
|
|
78
79
|
srcUrl: '',
|
|
79
80
|
title: 'Test Markup Include Title',
|
|
81
|
+
titleStyle: 'small',
|
|
80
82
|
useInlineHTML: true,
|
|
81
83
|
showNoDataMessage: false,
|
|
82
84
|
noDataMessageText: 'No data available'
|
|
@@ -89,7 +91,6 @@ const testConfig = {
|
|
|
89
91
|
legend: {},
|
|
90
92
|
newViz: true,
|
|
91
93
|
theme: 'theme-blue',
|
|
92
|
-
titleStyle: 'small',
|
|
93
94
|
showTitle: true,
|
|
94
95
|
type: 'markup-include',
|
|
95
96
|
visual: {
|
|
@@ -138,7 +139,7 @@ export const GeneralSectionTests: Story = {
|
|
|
138
139
|
'Title Update',
|
|
139
140
|
() => {
|
|
140
141
|
const modernTitle = canvasElement.querySelector('.cove-title')
|
|
141
|
-
const legacyTitle = canvasElement.querySelector('.cove-
|
|
142
|
+
const legacyTitle = canvasElement.querySelector('.cove-visualization__header h2')
|
|
142
143
|
const titleElement = modernTitle || legacyTitle
|
|
143
144
|
return titleElement?.textContent?.trim() || ''
|
|
144
145
|
},
|
|
@@ -150,10 +151,54 @@ export const GeneralSectionTests: Story = {
|
|
|
150
151
|
)
|
|
151
152
|
|
|
152
153
|
const modernHeader = canvasElement.querySelector('.cove-title')
|
|
153
|
-
const legacyHeader = canvasElement.querySelector('.cove-
|
|
154
|
+
const legacyHeader = canvasElement.querySelector('.cove-visualization__header h2')
|
|
154
155
|
const headerElement = modernHeader || legacyHeader
|
|
155
156
|
expect(headerElement).toBeTruthy()
|
|
156
157
|
expect(headerElement!.textContent?.trim()).toBe('Updated Markup Include Title E2E')
|
|
158
|
+
|
|
159
|
+
// ============================================================================
|
|
160
|
+
// TEST 2: Switch to TP5 Style
|
|
161
|
+
// Expectation: TP5 container appears and legacy title wrapper is removed
|
|
162
|
+
// ============================================================================
|
|
163
|
+
const styleSelect = canvasElement.querySelector('select[name="style"]') as HTMLSelectElement
|
|
164
|
+
expect(styleSelect).toBeTruthy()
|
|
165
|
+
|
|
166
|
+
await performAndAssert(
|
|
167
|
+
'Style Change to TP5',
|
|
168
|
+
() => ({
|
|
169
|
+
styleValue: styleSelect.value,
|
|
170
|
+
hasTp5Container: !!canvasElement.querySelector('.markup-include-component--tp5')
|
|
171
|
+
}),
|
|
172
|
+
async () => {
|
|
173
|
+
await userEvent.selectOptions(styleSelect, 'tp5')
|
|
174
|
+
},
|
|
175
|
+
(before, after) =>
|
|
176
|
+
before.styleValue !== after.styleValue &&
|
|
177
|
+
after.styleValue === 'tp5' &&
|
|
178
|
+
before.hasTp5Container !== after.hasTp5Container
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// ============================================================================
|
|
182
|
+
// TEST 3: TP5 title visibility
|
|
183
|
+
// Expectation: TP5 heading hidden when title is empty, shown when title has value
|
|
184
|
+
// ============================================================================
|
|
185
|
+
await performAndAssert(
|
|
186
|
+
'TP5 Title Hidden When Empty',
|
|
187
|
+
() => !!canvasElement.querySelector('.markup-include-tp5 .cdc-callout__heading span'),
|
|
188
|
+
async () => {
|
|
189
|
+
await userEvent.clear(titleInput)
|
|
190
|
+
},
|
|
191
|
+
(before, after) => before !== after && after === false
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
await performAndAssert(
|
|
195
|
+
'TP5 Title Shows With Text',
|
|
196
|
+
() => canvasElement.querySelector('.markup-include-tp5 .cdc-callout__heading span')?.textContent?.trim() || '',
|
|
197
|
+
async () => {
|
|
198
|
+
await userEvent.type(titleInput, 'TP5 Title Test')
|
|
199
|
+
},
|
|
200
|
+
(before, after) => before !== after && after === 'TP5 Title Test'
|
|
201
|
+
)
|
|
157
202
|
}
|
|
158
203
|
}
|
|
159
204
|
|
|
@@ -226,7 +271,7 @@ export const ContentEditorTests: Story = {
|
|
|
226
271
|
await performAndAssert(
|
|
227
272
|
'HTML Content Update',
|
|
228
273
|
() => {
|
|
229
|
-
const contentElement = canvasElement.querySelector('.cove-
|
|
274
|
+
const contentElement = canvasElement.querySelector('.cove-visualization__body')
|
|
230
275
|
return contentElement?.innerHTML || ''
|
|
231
276
|
},
|
|
232
277
|
async () => {
|
|
@@ -255,7 +300,7 @@ export const ContentEditorTests: Story = {
|
|
|
255
300
|
'Source URL Update and Content Loading',
|
|
256
301
|
() => ({
|
|
257
302
|
inputValue: srcUrlInput.value,
|
|
258
|
-
contentText: canvasElement.querySelector('.cove-
|
|
303
|
+
contentText: canvasElement.querySelector('.cove-visualization__body')?.textContent || ''
|
|
259
304
|
}),
|
|
260
305
|
async () => {
|
|
261
306
|
await userEvent.clear(srcUrlInput)
|
|
@@ -296,7 +341,7 @@ export const VisualSectionTests: Story = {
|
|
|
296
341
|
await waitForEditor(canvas)
|
|
297
342
|
await openAccordion(canvas, 'Visual')
|
|
298
343
|
|
|
299
|
-
const contentContainer = () => canvasElement.querySelector('.cove-
|
|
344
|
+
const contentContainer = () => canvasElement.querySelector('.cove-visualization__body') as HTMLElement
|
|
300
345
|
const visualContainer = () => canvasElement.querySelector('.markup-include-component') as HTMLElement
|
|
301
346
|
expect(contentContainer()).toBeTruthy()
|
|
302
347
|
expect(visualContainer()).toBeTruthy()
|
|
@@ -306,21 +351,16 @@ export const VisualSectionTests: Story = {
|
|
|
306
351
|
// Expectation: Theme class changes on component
|
|
307
352
|
// ============================================================================
|
|
308
353
|
const getThemeState = () => {
|
|
309
|
-
// Use the contentContainer like other tests, and check its parent for theme classes
|
|
310
354
|
const content = contentContainer()
|
|
311
355
|
if (!content) return { theme: '', classes: '', element: 'content not found' }
|
|
312
356
|
|
|
313
|
-
//
|
|
357
|
+
// Theme is applied to the outer cove-visualization wrapper — traverse up to find it
|
|
358
|
+
const themeWrapper = content.closest('[class*="theme-"]') as HTMLElement
|
|
359
|
+
const theme = themeWrapper ? Array.from(themeWrapper.classList).find(cls => cls.startsWith('theme-')) || '' : ''
|
|
360
|
+
|
|
314
361
|
const contentClasses = Array.from(content.classList).join(' ')
|
|
315
362
|
const parentClasses = content.parentElement ? Array.from(content.parentElement.classList).join(' ') : ''
|
|
316
363
|
|
|
317
|
-
const contentTheme = Array.from(content.classList).find(cls => cls.startsWith('theme-')) || ''
|
|
318
|
-
const parentTheme = content.parentElement
|
|
319
|
-
? Array.from(content.parentElement.classList).find(cls => cls.startsWith('theme-')) || ''
|
|
320
|
-
: ''
|
|
321
|
-
|
|
322
|
-
const theme = contentTheme || parentTheme || ''
|
|
323
|
-
|
|
324
364
|
return {
|
|
325
365
|
theme,
|
|
326
366
|
classes: contentClasses + ' | parent: ' + parentClasses,
|
|
@@ -380,7 +420,7 @@ export const VisualSectionTests: Story = {
|
|
|
380
420
|
'Border Color Theme Toggle',
|
|
381
421
|
() => ({
|
|
382
422
|
checked: borderColorThemeCheckbox.checked,
|
|
383
|
-
hasBorderColorTheme: visualContainer().classList.contains('component--has-
|
|
423
|
+
hasBorderColorTheme: visualContainer().classList.contains('component--has-border-color-theme')
|
|
384
424
|
}),
|
|
385
425
|
async () => {
|
|
386
426
|
const checkboxWrapper =
|
|
@@ -455,7 +495,7 @@ export const VisualSectionTests: Story = {
|
|
|
455
495
|
'Hide Background Color Toggle',
|
|
456
496
|
() => ({
|
|
457
497
|
checked: hideBackgroundCheckbox.checked,
|
|
458
|
-
hideBackground: visualContainer().classList.contains('component--
|
|
498
|
+
hideBackground: visualContainer().classList.contains('component--hide-background-color')
|
|
459
499
|
}),
|
|
460
500
|
async () => {
|
|
461
501
|
const checkboxWrapper = hideBackgroundCheckbox.closest('.cove-input__checkbox--small') || hideBackgroundCheckbox
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { expect } from 'storybook/test'
|
|
3
|
+
import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
|
|
4
|
+
import CdcMarkupInclude from '../CdcMarkupInclude'
|
|
5
|
+
import MinimalExampleConfig from '../../examples/minimal-example.json'
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof CdcMarkupInclude> = {
|
|
8
|
+
title: 'Components/Pages/Markup Include',
|
|
9
|
+
component: CdcMarkupInclude
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default meta
|
|
13
|
+
type Story = StoryObj<typeof CdcMarkupInclude>
|
|
14
|
+
|
|
15
|
+
export const Markup_Include_Minimal_Config: Story = {
|
|
16
|
+
args: {
|
|
17
|
+
config: MinimalExampleConfig,
|
|
18
|
+
isEditor: false
|
|
19
|
+
},
|
|
20
|
+
parameters: {
|
|
21
|
+
docs: {
|
|
22
|
+
description: {
|
|
23
|
+
story:
|
|
24
|
+
'Minimum working consumer config. This story validates the source-of-truth minimal example used by the package README and CONFIG reference.'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
play: async ({ canvasElement }) => {
|
|
29
|
+
await assertVisualizationRendered(canvasElement)
|
|
30
|
+
expect(canvasElement.textContent).toContain('Markup Include')
|
|
31
|
+
expect(canvasElement.textContent).toContain('Markup include minimum example.')
|
|
32
|
+
}
|
|
33
|
+
}
|