@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.
@@ -0,0 +1,11 @@
1
+ {
2
+ "type": "markup-include",
3
+ "version": "4.26.4",
4
+ "theme": "theme-blue",
5
+ "contentEditor": {
6
+ "inlineHTML": "<p>Markup include minimum example.</p>",
7
+ "srcUrl": "",
8
+ "title": "Markup Include",
9
+ "useInlineHTML": true
10
+ }
11
+ }
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@cdc/markup-include",
3
- "version": "4.26.2",
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.2",
9
+ "@cdc/core": "^4.26.4",
10
10
  "axios": "^1.13.2",
11
- "interweave": "^13.1.1",
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": "be3413e8e1149abf94225108f86a7910f56e0616",
22
+ "gitHead": "6097de1ff814001880d9ac64bd66becdc092d63c",
23
23
  "homepage": "https://github.com/CDCgov/cdc-open-viz#readme",
24
24
  "main": "dist/cdcmarkupinclude",
25
25
  "moduleName": "CdcMarkupInclude",
@@ -1,16 +1,18 @@
1
1
  import { useEffect, useCallback, useRef, useReducer, useMemo } from 'react'
2
- import _ from 'lodash'
3
2
  // external
4
- import { Markup } from 'interweave'
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 Layout from '@cdc/core/components/Layout'
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
- const { inlineHTML, srcUrl, title, useInlineHTML } = contentEditor || {}
88
+ const assignedDatasetData = config?.dataKey ? datasets?.[config.dataKey]?.data : undefined
89
+ if (Array.isArray(assignedDatasetData) && assignedDatasetData.length) {
90
+ return assignedDatasetData
91
+ }
81
92
 
82
- const shouldApplyTopPadding =
83
- visual?.border || visual?.background || (contentEditor?.title && contentEditor?.titleStyle === 'legacy')
84
- const shouldApplySidePadding = visual?.border || visual?.accent || visual?.background
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 dataString = await fetch(response.dataUrl)
116
- responseData = await dataString.json()
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
- * Transforms HTML by extracting <style> tags and applying their CSS rules as inline styles.
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 applyStyleTagsAsInlineStyles = (html: string): string => {
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
- try {
226
- // replaceSync parses the CSS and throws if invalid
227
- sheet.replaceSync(styleEl.textContent || '')
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
- // Apply each CSS rule to matching elements
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 markup = applyStyleTagsAsInlineStyles(processedMarkup.processedContent)
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
- content = (
303
- <>
304
- {isEditor && <EditorPanel datasets={datasets} />}
305
-
306
- {!hideMarkupInclude && (
307
- <Layout.Responsive isEditor={isEditor}>
308
- <div className='markup-include-content-container cove-component__content no-borders'>
309
- <Title
310
- title={title}
311
- isDashboard={isDashboard}
312
- titleStyle={contentEditor.titleStyle}
313
- config={config}
314
- classes={[`${theme}`, 'mb-0']}
315
- noContent={!markup}
316
- />
317
- <div className={`markup-include-component ${contentClasses.join(' ')}`}>
318
- <div className={`${innerContainerClasses.join(' ')}`}>
319
- {/* Filters */}
320
- {config.filters && config.filters.length > 0 && (
321
- <Filters
322
- config={config}
323
- setFilters={setFilters}
324
- excludedData={data || []}
325
- dimensions={[0, 0]}
326
- interactionLabel={interactionLabel || 'markup-include'}
327
- />
328
- )}
329
- <div
330
- className={`cove-component__content-wrap${shouldApplyTopPadding ? ' has-top-padding' : ''}${
331
- shouldApplySidePadding ? ' has-side-padding' : ''
332
- }`}
333
- >
334
- {_showNoDataMessage && (
335
- <div className='no-data-message'>
336
- <p>{`${noDataMessageText}`}</p>
337
- </div>
338
- )}
339
- {!markupError && !_showNoDataMessage && <Markup allowElements={!!urlMarkup} content={markup} />}
340
- {markupError && srcUrl && !_showNoDataMessage && <div className='warning'>{errorMessage}</div>}
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
- </div>
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
- </Layout.Responsive>
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 value={{ config, updateConfig, loading, data: data, setParentConfig, isDashboard }}>
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
- <Layout.VisualizationWrapper config={config} isEditor={isEditor} showEditorPanel={config?.showEditorPanel}>
453
+ <VisualizationContainer
454
+ config={config as any}
455
+ isEditor={isEditor}
456
+ editorPanel={<EditorPanel datasets={datasets} />}
457
+ >
374
458
  {content}
375
- </Layout.VisualizationWrapper>
459
+ </VisualizationContainer>
376
460
  </ConfigContext.Provider>
377
461
  </ErrorBoundary>
378
462
  )
@@ -4,6 +4,7 @@ import { MarkupIncludeConfig } from '@cdc/core/types/MarkupInclude'
4
4
  type ConfigCTX = {
5
5
  config: MarkupIncludeConfig
6
6
  data: Object[]
7
+ editorData?: Object[]
7
8
  isDashboard: boolean
8
9
  loading: boolean
9
10
  setParentConfig: Function
@@ -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-component__header h2')
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-component__header h2')
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-component__content')
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-component__content')?.textContent || ''
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-component__content') as HTMLElement
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
- // Check content itself and its parent for theme classes
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-borderColorTheme')
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--hideBackgroundColor')
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
+ }