@cdc/markup-include 4.26.3 → 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.3",
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.3",
9
+ "@cdc/core": "^4.26.4",
10
10
  "axios": "^1.13.2",
11
- "dompurify": "^3.3.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": "d50e45a074fbefa56cac904917e707d57f237737",
22
+ "gitHead": "6097de1ff814001880d9ac64bd66becdc092d63c",
23
23
  "homepage": "https://github.com/CDCgov/cdc-open-viz#readme",
24
24
  "main": "dist/cdcmarkupinclude",
25
25
  "moduleName": "CdcMarkupInclude",
@@ -2,12 +2,14 @@ import { useEffect, useCallback, useRef, useReducer, useMemo } from 'react'
2
2
  // external
3
3
  import DOMPurify from 'dompurify'
4
4
  import axios from 'axios'
5
+ import parse from 'html-react-parser'
5
6
 
6
7
  // cdc
7
8
  import { MarkupIncludeConfig } from '@cdc/core/types/MarkupInclude'
8
9
  import { publish } from '@cdc/core/helpers/events'
9
10
  import { processMarkupVariables } from '@cdc/core/helpers/markupProcessor'
10
11
  import { addValuesToFilters } from '@cdc/core/helpers/addValuesToFilters'
12
+ import { resolveDataColor } from '@cdc/core/helpers/dataColors'
11
13
  import ConfigContext from './ConfigContext'
12
14
  import coveUpdateWorker from '@cdc/core/helpers/coveUpdateWorker'
13
15
  import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
@@ -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,9 +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
+ }
87
+
88
+ const assignedDatasetData = config?.dataKey ? datasets?.[config.dataKey]?.data : undefined
89
+ if (Array.isArray(assignedDatasetData) && assignedDatasetData.length) {
90
+ return assignedDatasetData
91
+ }
92
+
93
+ return data || []
94
+ }, [config?.dataKey, data, datasets, isDashboard, isEditor, rawData])
79
95
 
80
- const { inlineHTML, srcUrl, title, useInlineHTML } = contentEditor || {}
96
+ const { inlineHTML, srcUrl, title, useInlineHTML, style: contentStyle } = contentEditor || {}
97
+ const markupIncludeStyle = contentStyle || 'default'
98
+ const isTp5Style = markupIncludeStyle === 'tp5'
81
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
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)
82
122
  // Default Functions
83
123
  const updateConfig = newConfig => {
84
124
  Object.keys(defaults).forEach(key => {
@@ -265,12 +305,47 @@ const CdcMarkupInclude: React.FC<CdcMarkupIncludeProps> = ({
265
305
  const hideMarkupInclude = processedMarkup.shouldHideSection
266
306
  const _showNoDataMessage = processedMarkup.shouldShowNoDataMessage
267
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
+
268
335
  if (loading === false) {
336
+ const hasTp5Title = processedTitle && processedTitle.trim()
269
337
  content = !hideMarkupInclude && (
270
338
  <VisualizationContent
271
339
  innerClassName={`markup-include-content-container ${innerContainerClasses.join(' ')}`.trim()}
272
- bodyClassName={`markup-include-component ${contentClasses.join(' ')}`.trim()}
273
- message={
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={
274
349
  config.filters && config.filters.length > 0 ? (
275
350
  <Filters
276
351
  config={config}
@@ -282,38 +357,78 @@ const CdcMarkupInclude: React.FC<CdcMarkupIncludeProps> = ({
282
357
  ) : null
283
358
  }
284
359
  header={
285
- <Title
286
- title={title}
287
- isDashboard={isDashboard}
288
- titleStyle={contentEditor?.titleStyle}
289
- config={config}
290
- classes={[`${theme}`, 'mb-0']}
291
- noContent={!sanitizedHTML}
292
- />
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
293
370
  }
294
371
  footer={
295
372
  <FootnotesStandAlone
296
- config={configObj?.footnotes}
373
+ config={config?.footnotes}
297
374
  filters={config?.filters || []}
298
375
  markupVariables={markupVariables}
299
376
  enableMarkupVariables={config?.enableMarkupVariables}
300
377
  data={data}
301
378
  dataMetadata={config?.dataMetadata}
379
+ footerClassName={isTp5Style ? 'mt-3' : undefined}
302
380
  />
303
381
  }
304
382
  >
305
- {_showNoDataMessage && (
306
- <div className='no-data-message'>
307
- <p>{`${noDataMessageText}`}</p>
308
- </div>
309
- )}
310
- {!markupError && !_showNoDataMessage && (
311
- <div id={scopeId}>
312
- {scopedCSS && <style>{scopedCSS}</style>}
313
- <div dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />
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 }} />
410
+ </div>
411
+ )}
412
+ {markupError && srcUrl && !_showNoDataMessage && <div className='warning'>{errorMessage}</div>}
413
+ </div>
414
+ </div>
314
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
+ </>
315
431
  )}
316
- {markupError && srcUrl && !_showNoDataMessage && <div className='warning'>{errorMessage}</div>}
317
432
  </VisualizationContent>
318
433
  )
319
434
  }
@@ -331,9 +446,15 @@ const CdcMarkupInclude: React.FC<CdcMarkupIncludeProps> = ({
331
446
 
332
447
  return (
333
448
  <ErrorBoundary component='CdcMarkupInclude'>
334
- <ConfigContext.Provider value={{ config, updateConfig, loading, data: data, setParentConfig, isDashboard }}>
449
+ <ConfigContext.Provider
450
+ value={{ config, updateConfig, loading, data: data, editorData, setParentConfig, isDashboard }}
451
+ >
335
452
  {!config?.newViz && config?.runtime && config?.runtime.editorErrorMessage && <Error />}
336
- <VisualizationContainer config={config} isEditor={isEditor} editorPanel={<EditorPanel datasets={datasets} />}>
453
+ <VisualizationContainer
454
+ config={config as any}
455
+ isEditor={isEditor}
456
+ editorPanel={<EditorPanel datasets={datasets} />}
457
+ >
337
458
  {content}
338
459
  </VisualizationContainer>
339
460
  </ConfigContext.Provider>
@@ -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: {
@@ -154,6 +155,50 @@ export const GeneralSectionTests: Story = {
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
 
@@ -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
+ }
@@ -1,5 +1,7 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { expect } from 'storybook/test'
2
3
  import CdcMarkupInclude from '../CdcMarkupInclude'
4
+ import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
3
5
  import primary from './_mock/primary.json'
4
6
  import noConditions from './_mock/no-conditions.json'
5
7
  import withConditions from './_mock/with-conditions.json'
@@ -48,6 +50,63 @@ export const icon_no_text: Story = {
48
50
  isEditor: false
49
51
  }
50
52
  }
53
+
54
+ export const Icon_Sizing: Story = {
55
+ args: {
56
+ config: {
57
+ ...primary,
58
+ contentEditor: {
59
+ ...primary.contentEditor,
60
+ showHeader: true,
61
+ title: 'SVG icon sizing',
62
+ useInlineHTML: true,
63
+ markupVariables: [
64
+ {
65
+ sourceType: 'icon',
66
+ name: 'link-external',
67
+ tag: '{{link-external}}',
68
+ iconId: 'link-external',
69
+ conditions: []
70
+ }
71
+ ],
72
+ inlineHTML: `
73
+ <p class="icon-sizing-inline">Rate <span class="icon-sizing-inline-target">{{link-external}}</span> increased</p>
74
+ <div
75
+ class="icon-sizing-parent-target"
76
+ data-cove-svg-size="parent-height"
77
+ style="height: 200px; background: #f8d7da; display: inline-flex; align-items: center;"
78
+ >
79
+ {{link-external}}
80
+ </div>
81
+ `
82
+ },
83
+ enableMarkupVariables: true
84
+ } as any,
85
+ isEditor: false
86
+ },
87
+ play: async ({ canvasElement }) => {
88
+ await assertVisualizationRendered(canvasElement)
89
+
90
+ const inlineIcon = canvasElement.querySelector('.icon-sizing-inline-target .cove-inline-svg') as HTMLElement | null
91
+ const parentIcon = canvasElement.querySelector('.icon-sizing-parent-target .cove-inline-svg') as HTMLElement | null
92
+ const parentContainer = canvasElement.querySelector('.icon-sizing-parent-target') as HTMLElement | null
93
+
94
+ expect(inlineIcon).toBeTruthy()
95
+ expect(parentIcon).toBeTruthy()
96
+ expect(parentContainer).toBeTruthy()
97
+
98
+ const inlineHeight = inlineIcon!.getBoundingClientRect().height
99
+ const parentHeight = parentIcon!.getBoundingClientRect().height
100
+ const containerHeight = parentContainer!.getBoundingClientRect().height
101
+
102
+ expect(inlineHeight).toBeGreaterThan(0)
103
+ expect(inlineHeight).toBeLessThan(50)
104
+ expect(parentHeight).toBeGreaterThan(inlineHeight * 3)
105
+ expect(parentHeight).toBeGreaterThan(180)
106
+ expect(Math.abs(parentHeight - containerHeight)).toBeLessThan(2)
107
+ }
108
+ }
109
+
51
110
  export const image_with_text: Story = {
52
111
  args: {
53
112
  config: imageWithText,
@@ -55,4 +114,26 @@ export const image_with_text: Story = {
55
114
  }
56
115
  }
57
116
 
117
+ export const TP5_Test: Story = {
118
+ args: {
119
+ config: {
120
+ ...primary,
121
+ contentEditor: {
122
+ ...primary.contentEditor,
123
+ style: 'tp5',
124
+ title: 'TP5 Markup Include Test',
125
+ useInlineHTML: true,
126
+ inlineHTML:
127
+ '<p>This is a TP5 style markup include test story.</p><p>Clear the title in the editor to verify TP5 title hide behavior.</p>'
128
+ },
129
+ visual: {
130
+ ...primary.visual,
131
+ border: true,
132
+ whiteBackground: true
133
+ }
134
+ },
135
+ isEditor: true
136
+ }
137
+ }
138
+
58
139
  export default meta