@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.
@@ -1,10 +1,11 @@
1
- import { useContext, useRef } from 'react'
1
+ import { useContext, useMemo, useRef } from 'react'
2
2
 
3
3
  // Context
4
4
  import ConfigContext from '../../ConfigContext'
5
5
 
6
6
  // Helpers
7
7
  import { updateFieldFactory } from '@cdc/core/helpers/updateFieldFactory'
8
+ import { useDataColumns } from '@cdc/core/hooks/useDataColumns'
8
9
 
9
10
  // Components
10
11
  import { EditorPanel as BaseEditorPanel } from '@cdc/core/components/EditorPanel/EditorPanel'
@@ -16,6 +17,9 @@ import MarkupVariablesEditor from '@cdc/core/components/EditorPanel/components/M
16
17
  import FootnotesEditor from '@cdc/core/components/EditorPanel/FootnotesEditor'
17
18
  import StyleTreatmentSection from '@cdc/core/components/EditorPanel/sections/StyleTreatmentSection'
18
19
  import { HeaderThemeSelector } from '@cdc/core/components/HeaderThemeSelector'
20
+ import { DataColorSelector } from '@cdc/core/components/DataColorSelector'
21
+ import { DATA_COLOR_PRESETS } from '@cdc/core/helpers/dataColors'
22
+ import Button from '@cdc/core/components/elements/Button'
19
23
  import { Datasets } from '@cdc/core/types/DataSet'
20
24
 
21
25
  // styles
@@ -26,11 +30,106 @@ type MarkupIncludeEditorPanelProps = {
26
30
  }
27
31
 
28
32
  const EditorPanel: React.FC<MarkupIncludeEditorPanelProps> = ({ datasets }) => {
29
- const { config, data, isDashboard, loading, setParentConfig, updateConfig } = useContext(ConfigContext)
33
+ const { config, data, editorData, isDashboard, loading, setParentConfig, updateConfig } = useContext(ConfigContext)
30
34
  const { contentEditor, theme, visual } = config || {}
31
35
  const { inlineHTML, srcUrl, title, useInlineHTML } = contentEditor || {}
36
+ const isTp5Style = contentEditor?.style === 'tp5'
32
37
  const updateField = updateFieldFactory(config, updateConfig, true)
33
- const styleTreatment = visual?.tp5Treatment ? 'tp5' : 'legacy'
38
+ const styleTreatment = (visual as any)?.tp5Treatment ? 'tp5' : 'legacy'
39
+ const markupEditorData = Array.isArray(editorData) ? editorData : data || []
40
+ const columns = useDataColumns(markupEditorData)
41
+ const dataColorMappings = config.dataColors?.mappings || []
42
+
43
+ const dataColorValues = useMemo(() => {
44
+ const colorColumn = config.dataColors?.column
45
+ if (!colorColumn) return []
46
+
47
+ const uniqueValues = new Set<string>()
48
+ markupEditorData?.forEach(row => {
49
+ const value = row?.[colorColumn]
50
+ if (value !== undefined && value !== null) {
51
+ uniqueValues.add(String(value))
52
+ }
53
+ })
54
+
55
+ return Array.from(uniqueValues).sort()
56
+ }, [markupEditorData, config.dataColors?.column])
57
+
58
+ type DataColorDisplayEntry = { sourceValue: string; fromData: boolean; key: string }
59
+
60
+ const dataColorDisplayList = useMemo<DataColorDisplayEntry[]>(() => {
61
+ const dataSet = new Set(dataColorValues)
62
+ const list: DataColorDisplayEntry[] = dataColorValues.map(v => ({
63
+ sourceValue: v,
64
+ fromData: true,
65
+ key: `data-${v}`
66
+ }))
67
+ dataColorMappings.forEach((m, i) => {
68
+ if (!dataSet.has(m.sourceValue)) {
69
+ list.push({ sourceValue: m.sourceValue, fromData: false, key: `custom-${i}` })
70
+ }
71
+ })
72
+ return list
73
+ }, [dataColorValues, dataColorMappings])
74
+
75
+ const updateDataColorMapping = (sourceValue: string, color: string) => {
76
+ const nextMappings = [...dataColorMappings]
77
+ const existingIndex = nextMappings.findIndex(m => m.sourceValue === sourceValue)
78
+
79
+ if (!color) {
80
+ if (existingIndex > -1) {
81
+ nextMappings.splice(existingIndex, 1)
82
+ }
83
+ } else {
84
+ const nextMapping = { sourceValue, color }
85
+ if (existingIndex > -1) {
86
+ nextMappings[existingIndex] = nextMapping
87
+ } else {
88
+ nextMappings.push(nextMapping)
89
+ }
90
+ }
91
+
92
+ updateConfig({
93
+ ...config,
94
+ dataColors: {
95
+ ...config.dataColors,
96
+ mappings: nextMappings
97
+ }
98
+ })
99
+ }
100
+
101
+ const addCustomDataColorMapping = () => {
102
+ const nextMappings = [...dataColorMappings, { sourceValue: '', color: DATA_COLOR_PRESETS[0] }]
103
+ updateConfig({
104
+ ...config,
105
+ dataColors: {
106
+ ...config.dataColors,
107
+ mappings: nextMappings
108
+ }
109
+ })
110
+ }
111
+
112
+ const updateDataColorMappingValue = (oldValue: string, newValue: string) => {
113
+ const nextMappings = dataColorMappings.map(m => (m.sourceValue === oldValue ? { ...m, sourceValue: newValue } : m))
114
+ updateConfig({
115
+ ...config,
116
+ dataColors: {
117
+ ...config.dataColors,
118
+ mappings: nextMappings
119
+ }
120
+ })
121
+ }
122
+
123
+ const removeDataColorMapping = (sourceValue: string) => {
124
+ const nextMappings = dataColorMappings.filter(m => m.sourceValue !== sourceValue)
125
+ updateConfig({
126
+ ...config,
127
+ dataColors: {
128
+ ...config.dataColors,
129
+ mappings: nextMappings
130
+ }
131
+ })
132
+ }
34
133
 
35
134
  const handleStyleTreatmentChange = (value: string) => {
36
135
  const useTp5Treatment = value === 'tp5'
@@ -74,6 +173,17 @@ const EditorPanel: React.FC<MarkupIncludeEditorPanelProps> = ({ datasets }) => {
74
173
  {() => (
75
174
  <Accordion>
76
175
  <Accordion.Section title='General'>
176
+ <Select
177
+ value={contentEditor?.style || 'default'}
178
+ section='contentEditor'
179
+ fieldName='style'
180
+ label='Style'
181
+ updateField={updateField}
182
+ options={[
183
+ { value: 'default', label: 'Default' },
184
+ { value: 'tp5', label: 'TP5' }
185
+ ]}
186
+ />
77
187
  <TextField
78
188
  value={title || ''}
79
189
  section='contentEditor'
@@ -82,32 +192,34 @@ const EditorPanel: React.FC<MarkupIncludeEditorPanelProps> = ({ datasets }) => {
82
192
  placeholder='Markup Include Title'
83
193
  updateField={updateField}
84
194
  />
85
- <Select
86
- value={contentEditor?.titleStyle || 'small'}
87
- section='contentEditor'
88
- fieldName='titleStyle'
89
- label='Title Style'
90
- updateField={updateField}
91
- options={[
92
- { value: 'small', label: 'Small (h3)' },
93
- { value: 'large', label: 'Large (h2)' },
94
- { value: 'legacy', label: 'Legacy' }
95
- ]}
96
- tooltip={
97
- <Tooltip style={{ textTransform: 'none' }}>
98
- <Tooltip.Target>
99
- <Icon display='question' style={{ marginLeft: '0.5rem' }} />
100
- </Tooltip.Target>
101
- <Tooltip.Content>
102
- <p>Choose the visual style for the title.</p>
103
- <p>
104
- Consider heading order on your page when selecting the title style. For 508 reasons, ensure your
105
- page follows a proper heading order.
106
- </p>
107
- </Tooltip.Content>
108
- </Tooltip>
109
- }
110
- />
195
+ {!isTp5Style && (
196
+ <Select
197
+ value={contentEditor?.titleStyle || 'small'}
198
+ section='contentEditor'
199
+ fieldName='titleStyle'
200
+ label='Title Style'
201
+ updateField={updateField}
202
+ options={[
203
+ { value: 'small', label: 'Small (h3)' },
204
+ { value: 'large', label: 'Large (h2)' },
205
+ { value: 'legacy', label: 'Legacy' }
206
+ ]}
207
+ tooltip={
208
+ <Tooltip style={{ textTransform: 'none' }}>
209
+ <Tooltip.Target>
210
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
211
+ </Tooltip.Target>
212
+ <Tooltip.Content>
213
+ <p>Choose the visual style for the title.</p>
214
+ <p>
215
+ Consider heading order on your page when selecting the title style. For 508 reasons, ensure your
216
+ page follows a proper heading order.
217
+ </p>
218
+ </Tooltip.Content>
219
+ </Tooltip>
220
+ }
221
+ />
222
+ )}
111
223
  <Select
112
224
  value={config.locale}
113
225
  fieldName='locale'
@@ -170,24 +282,111 @@ const EditorPanel: React.FC<MarkupIncludeEditorPanelProps> = ({ datasets }) => {
170
282
  </div>
171
283
  </Accordion.Section>
172
284
  <Accordion.Section title='Visual'>
173
- <HeaderThemeSelector
174
- selectedTheme={config.theme}
175
- onThemeSelect={theme => updateConfig({ ...config, theme })}
176
- />
177
- <StyleTreatmentSection
178
- styleTreatment={styleTreatment}
179
- onStyleTreatmentChange={handleStyleTreatmentChange}
180
- showStyleTreatment={false}
181
- border={config.visual?.border}
182
- borderColorTheme={config.visual?.borderColorTheme}
183
- accent={config.visual?.accent}
184
- background={config.visual?.background}
185
- hideBackgroundColor={config.visual?.hideBackgroundColor}
186
- showBackground
187
- showHideBackgroundColor
188
- updateField={updateField}
189
- />
285
+ {!isTp5Style && (
286
+ <HeaderThemeSelector
287
+ selectedTheme={config.theme}
288
+ onThemeSelect={theme => updateConfig({ ...config, theme })}
289
+ />
290
+ )}
291
+ {isTp5Style ? (
292
+ <CheckBox
293
+ value={visual?.whiteBackground}
294
+ section='visual'
295
+ fieldName='whiteBackground'
296
+ label='Use White Background Style'
297
+ updateField={updateField}
298
+ />
299
+ ) : (
300
+ <StyleTreatmentSection
301
+ styleTreatment={styleTreatment}
302
+ onStyleTreatmentChange={handleStyleTreatmentChange}
303
+ showStyleTreatment={false}
304
+ border={config.visual?.border}
305
+ borderColorTheme={config.visual?.borderColorTheme}
306
+ accent={config.visual?.accent}
307
+ background={config.visual?.background}
308
+ hideBackgroundColor={config.visual?.hideBackgroundColor}
309
+ showBackground
310
+ showHideBackgroundColor
311
+ updateField={updateField}
312
+ />
313
+ )}
190
314
  </Accordion.Section>
315
+ {isTp5Style && (
316
+ <Accordion.Section title='Data-Driven Colors'>
317
+ <Select
318
+ value={config.dataColors?.column || ''}
319
+ section='dataColors'
320
+ fieldName='column'
321
+ label='Color Column'
322
+ updateField={updateField}
323
+ initial='Select'
324
+ options={columns}
325
+ tooltip={
326
+ <Tooltip style={{ textTransform: 'none' }}>
327
+ <Tooltip.Target>
328
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
329
+ </Tooltip.Target>
330
+ <Tooltip.Content>
331
+ <p>
332
+ Choose a column whose values determine the background color of this visualization. Map each
333
+ value to a color below. Text color adjusts automatically for contrast.
334
+ </p>
335
+ </Tooltip.Content>
336
+ </Tooltip>
337
+ }
338
+ />
339
+ {config.dataColors?.column && dataColorDisplayList.length > 0 && (
340
+ <div className='mt-2'>
341
+ {dataColorDisplayList.map(({ sourceValue, fromData, key }) => {
342
+ const selectedColor = dataColorMappings.find(m => m.sourceValue === sourceValue)?.color || ''
343
+
344
+ return (
345
+ <div className='cove-accordion__panel-row align-center mb-2' key={key}>
346
+ <div className='cove-accordion__panel-col' style={{ flex: '1 1 0', minWidth: 0 }}>
347
+ {fromData ? (
348
+ sourceValue
349
+ ) : (
350
+ <input
351
+ type='text'
352
+ value={sourceValue}
353
+ placeholder='Enter value'
354
+ style={{ width: '100%' }}
355
+ onChange={e => updateDataColorMappingValue(sourceValue, e.target.value)}
356
+ />
357
+ )}
358
+ </div>
359
+ <div className='cove-accordion__panel-col' style={{ flex: '0 0 4.5rem' }}>
360
+ <DataColorSelector
361
+ value={selectedColor}
362
+ onChange={color => updateDataColorMapping(sourceValue, color)}
363
+ />
364
+ </div>
365
+ <div className='cove-accordion__panel-col' style={{ flex: '0 0 1.5rem' }}>
366
+ {!fromData && (
367
+ <button
368
+ type='button'
369
+ className='btn btn-danger'
370
+ style={{ padding: '0.15rem 0.45rem', lineHeight: 1 }}
371
+ title='Remove mapping'
372
+ onClick={() => removeDataColorMapping(sourceValue)}
373
+ >
374
+
375
+ </button>
376
+ )}
377
+ </div>
378
+ </div>
379
+ )
380
+ })}
381
+ </div>
382
+ )}
383
+ {config.dataColors?.column && (
384
+ <Button type='button' onClick={addCustomDataColorMapping} className='btn btn-primary full-width mt-3'>
385
+ Add Color Mapping
386
+ </Button>
387
+ )}
388
+ </Accordion.Section>
389
+ )}
191
390
  {isDashboard && (
192
391
  <Accordion.Section title='Footnotes'>
193
392
  <FootnotesEditor config={config} updateField={updateField} datasets={datasets} />
@@ -196,7 +395,7 @@ const EditorPanel: React.FC<MarkupIncludeEditorPanelProps> = ({ datasets }) => {
196
395
  <Accordion.Section title='Markup Variables'>
197
396
  <MarkupVariablesEditor
198
397
  markupVariables={config.markupVariables || []}
199
- data={data || []}
398
+ data={markupEditorData}
200
399
  datasets={datasets}
201
400
  config={config}
202
401
  onChange={handleMarkupVariablesChange}
@@ -2,6 +2,7 @@ export default {
2
2
  contentEditor: {
3
3
  inlineHTML: '<strong>Inline HTML</strong>',
4
4
  showHeader: true,
5
+ style: 'default',
5
6
  srcUrl: '#example',
6
7
  title: '',
7
8
  titleStyle: 'small',
@@ -22,7 +23,12 @@ export default {
22
23
  accent: false,
23
24
  background: false,
24
25
  hideBackgroundColor: false,
25
- borderColorTheme: false
26
+ borderColorTheme: false,
27
+ whiteBackground: false
28
+ },
29
+ dataColors: {
30
+ column: '',
31
+ mappings: []
26
32
  },
27
33
  markupVariables: [],
28
34
  enableMarkupVariables: false
@@ -5,6 +5,73 @@
5
5
  @include cove-visualization-body-padding;
6
6
  }
7
7
 
8
+ .markup-include-component--tp5 {
9
+ padding: 0 !important;
10
+ border: none !important;
11
+ background: none !important;
12
+ container-type: inline-size;
13
+
14
+ .markup-include-tp5 {
15
+ gap: 0.7rem;
16
+ box-shadow: 0 2px 4px rgb(159 159 159 / 10%);
17
+ border: 1px solid var(--colors-cyan-15, #dff2f6) !important;
18
+ margin: 0 !important;
19
+ padding: 1.25rem;
20
+ border-radius: 0.25rem;
21
+ background-color: var(--colors-cyan-10, #eff9fa);
22
+ }
23
+
24
+ .cdc-callout__heading {
25
+ width: 100%;
26
+ font-size: 1.1rem;
27
+ }
28
+
29
+ .cdc-callout__body {
30
+ flex-wrap: nowrap;
31
+ flex: auto;
32
+ }
33
+
34
+ @container (max-width: 576px) {
35
+ .cdc-callout__body {
36
+ flex-wrap: wrap;
37
+ }
38
+ }
39
+
40
+ .cdc-callout__content {
41
+ font-size: 1rem;
42
+ }
43
+
44
+ .cdc-callout--data-color {
45
+ .cdc-callout__heading,
46
+ .cdc-callout__content {
47
+ color: inherit;
48
+ }
49
+ }
50
+
51
+ &.white-background-style {
52
+ .markup-include-tp5 {
53
+ border: 1px solid #009ec1 !important;
54
+ background: transparent;
55
+ box-shadow: 0 2px 4px rgb(159 159 159 / 10%);
56
+ }
57
+
58
+ .cdc-callout--data-color {
59
+ border: none !important;
60
+ }
61
+ }
62
+
63
+ &.white-background-style.display-border {
64
+ .markup-include-tp5 {
65
+ box-shadow: 0 2px 4px rgb(159 159 159 / 10%);
66
+ }
67
+ }
68
+
69
+ .cove-visualization__body-wrap,
70
+ .cove-visualization__content-section {
71
+ padding: 0;
72
+ }
73
+ }
74
+
8
75
  .cove-visualization__body-wrap {
9
76
  @include cove-visualization-body-wrap-inline-padding;
10
77
 
@@ -22,7 +89,7 @@
22
89
  // keep markup include images inlines on dashboards
23
90
  // e.g. requested to help here: https://www.cdc.gov/fluview/surveillance/2026-week-10.html
24
91
  // needed for backwards compatibility with existing dashboards, but also to prevent any future issues with images in markup include visualizations being forced to display as blocks
25
- span>img {
92
+ span > img {
26
93
  display: inline;
27
94
  }
28
95
  }
@@ -54,4 +121,4 @@
54
121
  .cove-editor .cove-editor__content {
55
122
  padding-left: 350px;
56
123
  display: flex;
57
- }
124
+ }
@@ -1,6 +1,25 @@
1
1
  import path from 'node:path'
2
+ import fs from 'node:fs'
3
+ import vm from 'node:vm'
4
+ import React from 'react'
5
+ import { render, screen } from '@testing-library/react'
2
6
  import { testStandaloneBuild } from '@cdc/core/helpers/tests/testStandaloneBuild.ts'
3
- import { describe, it, expect } from 'vitest'
7
+ import { describe, it, expect, vi } from 'vitest'
8
+ import CdcMarkupInclude from '../CdcMarkupInclude'
9
+
10
+ vi.mock('@cdc/core/components/EditorPanel/components/MarkupVariablesEditor', () => ({
11
+ default: ({ data }) => <div data-testid='markup-variables-editor-data'>{JSON.stringify(data)}</div>
12
+ }))
13
+
14
+ const extractMarkedExampleConfig = (content, label) => {
15
+ const match = content.match(
16
+ /<!-- README_EXAMPLE_CONFIG_START -->\s*```jsx\s*([\s\S]*?)\s*```\s*<!-- README_EXAMPLE_CONFIG_END -->/
17
+ )
18
+ expect(match, `${label} should contain a marked README example block`).toBeTruthy()
19
+ const configMatch = match[1].match(/const config = (\{[\s\S]*?\})\n\nfunction App\(\)/)
20
+ expect(configMatch, `${label} should define const config before function App()`).toBeTruthy()
21
+ return vm.runInNewContext(`(${configMatch[1]})`)
22
+ }
4
23
 
5
24
  describe('Markup Include', () => {
6
25
  it('Can be built in isolation', async () => {
@@ -8,4 +27,56 @@ describe('Markup Include', () => {
8
27
  const result = await testStandaloneBuild(pkgDir)
9
28
  expect(result).toBe(true)
10
29
  }, 300000)
30
+
31
+ it('uses dashboard rawData for markup variable editor choices when editing', async () => {
32
+ const filteredData = [{ category: 'Filtered only' }]
33
+ const fullData = [{ category: 'Value A' }, { category: 'Value B' }]
34
+
35
+ render(
36
+ <CdcMarkupInclude
37
+ config={{
38
+ type: 'markup-include',
39
+ theme: 'theme-blue',
40
+ dataKey: 'test-dataset',
41
+ data: filteredData,
42
+ markupVariables: [],
43
+ contentEditor: {
44
+ title: '',
45
+ inlineHTML: '<p>Example</p>',
46
+ useInlineHTML: true,
47
+ srcUrl: ''
48
+ },
49
+ visual: {
50
+ border: false,
51
+ accent: false,
52
+ background: false,
53
+ hideBackgroundColor: false,
54
+ borderColorTheme: false
55
+ }
56
+ }}
57
+ datasets={{
58
+ 'test-dataset': {
59
+ data: [{ category: 'Dataset fallback' }]
60
+ }
61
+ }}
62
+ rawData={fullData}
63
+ isDashboard={true}
64
+ isEditor={true}
65
+ />
66
+ )
67
+
68
+ expect(JSON.parse((await screen.findByTestId('markup-variables-editor-data')).textContent)).toEqual(fullData)
69
+ })
70
+
71
+ it('keeps the minimal example in sync with the README docs', () => {
72
+ const pkgRoot = path.join(__dirname, '..', '..')
73
+ const minimalExamplePath = path.join(pkgRoot, 'examples', 'minimal-example.json')
74
+ const readmePath = path.join(pkgRoot, 'README.md')
75
+
76
+ const minimalExample = JSON.parse(fs.readFileSync(minimalExamplePath, 'utf8'))
77
+ const readmeBlock = extractMarkedExampleConfig(fs.readFileSync(readmePath, 'utf8'), 'README.md')
78
+
79
+ expect(readmeBlock).toEqual(minimalExample)
80
+ expect(minimalExample.version).toBeTruthy()
81
+ })
11
82
  })