@cdc/map 4.25.11 → 4.26.1

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.
Files changed (44) hide show
  1. package/dist/cdcmap.js +29879 -29091
  2. package/examples/private/city_styles_variable.json +877 -0
  3. package/examples/private/map-filter-issue.json +2260 -0
  4. package/examples/private/map-legend.json +5303 -0
  5. package/index.html +27 -37
  6. package/package.json +5 -4
  7. package/src/CdcMapComponent.tsx +42 -6
  8. package/src/_stories/CdcMap.Editor.stories.tsx +92 -37
  9. package/src/_stories/CdcMap.stories.tsx +94 -0
  10. package/src/_stories/_mock/usa-state-gradient.json +1 -0
  11. package/src/components/CityList.tsx +24 -18
  12. package/src/components/EditorPanel/components/EditorPanel.tsx +2320 -2212
  13. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +0 -19
  14. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +14 -17
  15. package/src/components/Legend/components/Legend.tsx +24 -41
  16. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +1 -1
  17. package/src/components/Legend/components/index.scss +22 -5
  18. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +6 -5
  19. package/src/components/UsaMap/components/Territory/TerritoryShape.ts +1 -0
  20. package/src/components/UsaMap/components/UsaMap.County.tsx +2 -2
  21. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +4 -7
  22. package/src/components/UsaMap/components/UsaMap.State.tsx +4 -2
  23. package/src/data/initial-state.js +1 -0
  24. package/src/helpers/applyLegendToRow.ts +5 -3
  25. package/src/helpers/constants.ts +2 -0
  26. package/src/helpers/displayGeoName.ts +8 -5
  27. package/src/helpers/generateRuntimeFilters.ts +1 -1
  28. package/src/helpers/generateRuntimeLegend.ts +1 -1
  29. package/src/helpers/generateRuntimeLegendHash.ts +1 -1
  30. package/src/helpers/index.ts +9 -3
  31. package/src/helpers/isLegendItemDisabled.ts +2 -2
  32. package/src/helpers/resetLegendToggles.ts +1 -0
  33. package/src/helpers/tests/hashObj.test.ts +1 -1
  34. package/src/helpers/toggleLegendActive.ts +76 -8
  35. package/src/hooks/useResizeObserver.ts +3 -0
  36. package/src/hooks/useStateZoom.tsx +2 -2
  37. package/src/test/CdcMap.test.jsx +1 -1
  38. package/src/types/MapConfig.ts +2 -0
  39. package/src/types/runtimeLegend.ts +1 -0
  40. package/LICENSE +0 -201
  41. package/src/components/MapControls.tsx +0 -44
  42. package/src/helpers/getUniqueValues.ts +0 -19
  43. package/src/helpers/hashObj.ts +0 -25
  44. package/src/hooks/useLegendSeparators.ts +0 -26
package/index.html CHANGED
@@ -1,42 +1,33 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
6
+ <style type="text/css">
7
+ body {
8
+ margin: 0;
9
+ border-top: none !important;
10
+ /* Force scrollbar so page stays consistent width */
11
+ min-height: calc(100vh + 1px);
12
+ }
3
13
 
4
- <head>
5
- <meta charset="utf-8" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
7
- <style type="text/css">
8
- body {
9
- margin: 0;
10
- border-top: none !important;
11
- }
14
+ /* Add 1rem padding to mimic DFE when editor is not visible */
15
+ .cdc-open-viz-module:not(.isEditor) {
16
+ padding: 1rem;
17
+ }
18
+ </style>
19
+ <link rel="stylesheet prefetch" href="https://www.cdc.gov/TemplatePackage/5.0/css/app.min.css?_=71669" />
20
+ </head>
12
21
 
13
- .cdc-map-outer-container {
14
- min-height: 100vh;
15
- }
16
- </style>
17
- <link rel="stylesheet prefetch" href="https://www.cdc.gov/TemplatePackage/5.0/css/app.min.css?_=71669" />
22
+ <body>
23
+ <!-- DEFAULT EXAMPLES -->
24
+ <!-- <div class="react-container" data-config="/examples/example-city-state.json"></div> -->
25
+ <div class="react-container" data-config="/examples/default-single-state.json"></div>
18
26
 
19
- <!-- This is temporary and for testing until Nunito/900 is added to TP -->
20
- <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@900&display=swap" rel="stylesheet" />
21
- <style type="text/css">
22
- @font-face {
23
- font-family: 'Nunito';
24
- font-weight: 900;
25
- font-display: swap;
26
- src: url('https://app.unpkg.com/@fontsource/nunito@5.0.18/files/files/nunito-latin-900-normal.woff2') format('woff2');
27
- }
28
- </style>
29
- </head>
30
-
31
- <body>
32
- <!-- DEFAULT EXAMPLES -->
33
- <!-- <div class="react-container" data-config="/examples/example-city-state.json"></div> -->
34
- <div class="react-container" data-config="/examples/private/measles.json"></div>
35
-
36
- <noscript>You need to enable JavaScript to run this app.</noscript>
37
- <script type="module" src="./src/index.jsx"></script>
38
- <!-- add cove_loaded listener -->
39
- <!-- <script>
27
+ <noscript>You need to enable JavaScript to run this app.</noscript>
28
+ <script type="module" src="./src/index.jsx"></script>
29
+ <!-- add cove_loaded listener -->
30
+ <!-- <script>
40
31
  document.addEventListener('cove_loaded', function () {
41
32
  // This is a temporary fix to ensure the map loads after Cove has loaded
42
33
  // and the cdc-map-outer-container is available.
@@ -44,6 +35,5 @@
44
35
  console.log('Cove has loaded, initializing map...');
45
36
  });
46
37
  </script> -->
47
- </body>
48
-
49
- </html>
38
+ </body>
39
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cdc/map",
3
- "version": "4.25.11",
3
+ "version": "4.26.1",
4
4
  "description": "React component for visualizing tabular data on a map of the United States or the world.",
5
5
  "moduleName": "CdcMap",
6
6
  "main": "dist/cdcmap",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "license": "Apache-2.0",
27
27
  "dependencies": {
28
- "@cdc/core": "^4.25.11",
28
+ "@cdc/core": "^4.26.1",
29
29
  "@googlemaps/markerclusterer": "^2.5.3",
30
30
  "@hello-pangea/dnd": "^16.2.0",
31
31
  "@react-google-maps/api": "^2.20.6",
@@ -55,16 +55,17 @@
55
55
  "react-icons": "5.5.0",
56
56
  "react-tooltip": "5.8.2-beta.3",
57
57
  "resize-observer-polyfill": "^1.5.1",
58
+ "sass": "^1.89.2",
58
59
  "topojson-client": "^3.1.0",
59
60
  "use-debounce": "^10.0.5",
60
61
  "vite": "^5.4.21",
61
62
  "vite-plugin-css-injected-by-js": "^2.4.0",
62
- "vite-plugin-svgr": "^2.4.0",
63
+ "vite-plugin-svgr": "^4.2.0",
63
64
  "whatwg-fetch": "3.6.20"
64
65
  },
65
66
  "peerDependencies": {
66
67
  "react": "^18.2.0",
67
68
  "react-dom": "^18.2.0"
68
69
  },
69
- "gitHead": "5f09a137c22f454111ab5f4cd7fdf1d2d58e31bd"
70
+ "gitHead": "7e3b27098c4eb7a24bc9c3654ad53f88d6419f16"
70
71
  }
@@ -9,6 +9,7 @@ import 'react-tooltip/dist/react-tooltip.css'
9
9
  import DataTable from '@cdc/core/components/DataTable'
10
10
  import Filters from '@cdc/core/components/Filters'
11
11
  import Layout from '@cdc/core/components/Layout'
12
+ import MediaControls from '@cdc/core/components/MediaControls'
12
13
  import SkipTo from '@cdc/core/components/elements/SkipTo'
13
14
  import Title from '@cdc/core/components/ui/Title'
14
15
  import Waiting from '@cdc/core/components/Waiting'
@@ -23,7 +24,7 @@ import './scss/main.scss'
23
24
  import './cdcMapComponent.styles.css'
24
25
 
25
26
  // Core Helpers
26
- import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
27
+ import { getQueryStringFilterValue, isFilterHiddenByQuery } from '@cdc/core/helpers/queryStringUtils'
27
28
  import { generateRuntimeFilters } from './helpers/generateRuntimeFilters'
28
29
  import { type MapReducerType, MapState } from './store/map.reducer'
29
30
  import { addValuesToFilters } from '@cdc/core/helpers/addValuesToFilters'
@@ -37,9 +38,11 @@ import {
37
38
  getMapContainerClasses,
38
39
  generateRuntimeLegendHash,
39
40
  handleMapTabbing,
40
- hashObj,
41
41
  navigationHandler
42
42
  } from './helpers'
43
+ import { hashObj } from '@cdc/core/helpers/hashObj'
44
+ import { applyLegendToRow } from './helpers/applyLegendToRow'
45
+ import { getPatternForRow } from './helpers/getPatternForRow'
43
46
  import { generateRuntimeLegend } from './helpers/generateRuntimeLegend'
44
47
  import generateRuntimeData from './helpers/generateRuntimeData'
45
48
  import { reloadURLData } from './helpers/urlDataHelpers'
@@ -55,7 +58,6 @@ import EditorPanel from './components/EditorPanel'
55
58
  import Error from './components/EditorPanel/components/Error'
56
59
  import Legend from './components/Legend'
57
60
  import MapContainer from './components/MapContainer'
58
- import MapControls from './components/MapControls'
59
61
  import NavigationMenu from './components/NavigationMenu'
60
62
 
61
63
  // hooks
@@ -196,6 +198,9 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
196
198
  if (queryStringFilterValue) {
197
199
  filters[index].active = queryStringFilterValue
198
200
  }
201
+ if (isFilterHiddenByQuery(filter)) {
202
+ filters[index].showDropdown = false
203
+ }
199
204
  })
200
205
  dispatch({ type: 'SET_RUNTIME_FILTERS', payload: filters })
201
206
  }
@@ -430,6 +435,8 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
430
435
  <Title
431
436
  title={title}
432
437
  superTitle={processedSuperTitle}
438
+ titleStyle={general.titleStyle}
439
+ showTitle={general.showTitle}
433
440
  config={config}
434
441
  classes={['map-title', general.showTitle === true ? 'visible' : 'hidden', `${headerColor}`]}
435
442
  />
@@ -503,9 +510,8 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
503
510
 
504
511
  {processedSubtext.length > 0 && <p className='subtext mt-4'>{parse(processedSubtext)}</p>}
505
512
 
506
- <MapControls config={config} imageId={imageId} interactionLabel={interactionLabel} />
507
-
508
- {shouldShowDataTable(config, table, general, loading) && (
513
+ {/* Data Table or Download Links */}
514
+ {shouldShowDataTable(config, table, general, loading) ? (
509
515
  <DataTable
510
516
  columns={dataTableColumns}
511
517
  config={dataTableConfig}
@@ -528,12 +534,42 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
528
534
  runtimeLegend={runtimeLegend}
529
535
  showDownloadImgButton={showDownloadImgButton}
530
536
  showDownloadPdfButton={showDownloadPdfButton}
537
+ includeContextInDownload={config.general?.includeContextInDownload}
531
538
  tabbingId={tabId}
532
539
  tableTitle={table.label}
533
540
  vizTitle={general.title}
541
+ applyLegendToRow={applyLegendToRow}
542
+ getPatternForRow={getPatternForRow}
534
543
  wrapColumns={table.wrapColumns}
535
544
  interactionLabel={interactionLabel}
536
545
  />
546
+ ) : (
547
+ (showDownloadImgButton || showDownloadPdfButton) && (
548
+ <div className='w-100 d-flex justify-content-end'>
549
+ <MediaControls.Section classes={['download-links', 'mt-4', 'mb-2']}>
550
+ {showDownloadImgButton && (
551
+ <MediaControls.DownloadLink
552
+ type='image'
553
+ title='Download Map as Image'
554
+ state={config}
555
+ elementToCapture={imageId}
556
+ interactionLabel={interactionLabel}
557
+ includeContextInDownload={config.general?.includeContextInDownload}
558
+ />
559
+ )}
560
+ {showDownloadPdfButton && (
561
+ <MediaControls.DownloadLink
562
+ type='pdf'
563
+ title='Download Map as PDF'
564
+ state={config}
565
+ elementToCapture={imageId}
566
+ interactionLabel={interactionLabel}
567
+ includeContextInDownload={config.general?.includeContextInDownload}
568
+ />
569
+ )}
570
+ </MediaControls.Section>
571
+ </div>
572
+ )
537
573
  )}
538
574
 
539
575
  {config.annotations?.length > 0 && <Annotation.Dropdown />}
@@ -119,7 +119,10 @@ export const TypeSectionTests: Story = {
119
119
  // ==========================================================================
120
120
  // TEST: Map Type select toggles classes/data representation
121
121
  // ==========================================================================
122
- const typeSelect = canvas.getByLabelText(/Map Type/i) as HTMLSelectElement
122
+ // Use getAllByLabelText to avoid multiple elements error
123
+ const typeSelects = canvas.getAllByLabelText(/Map Type/i, { selector: 'select' }) as HTMLSelectElement[]
124
+ // Assume the first select is the correct one for the Type section
125
+ const typeSelect = typeSelects[0]
123
126
  const initialType = typeSelect.value
124
127
  const mapTypeOptions = Array.from(typeSelect.options).map(option => option.value)
125
128
  expect(mapTypeOptions).toContain('navigation')
@@ -324,7 +327,7 @@ export const GeneralSectionTests: Story = {
324
327
  expect(titleInput).toBeTruthy()
325
328
 
326
329
  const getTitleVisual = () => {
327
- const titleElement = canvasElement.querySelector('.map-title')
330
+ const titleElement = canvasElement.querySelector('.cove-title, .map-title')
328
331
  return {
329
332
  titleText: titleElement?.textContent || '',
330
333
  hasTitleElement: Boolean(titleElement)
@@ -343,7 +346,7 @@ export const GeneralSectionTests: Story = {
343
346
 
344
347
  // ==========================================================================
345
348
  // TEST: Show Title checkbox
346
- // Verifies: Title element visibility changes (visible/hidden class)
349
+ // Verifies: Title element visibility is controlled by showTitle
347
350
  // ==========================================================================
348
351
  const generalAccordion = canvasElement.querySelector('[aria-expanded="true"]')?.closest('.accordion__item')
349
352
  const showTitleLabel = Array.from(generalAccordion?.querySelectorAll('label') || []).find(label =>
@@ -353,22 +356,20 @@ export const GeneralSectionTests: Story = {
353
356
  expect(showTitleCheckbox).toBeTruthy()
354
357
 
355
358
  const getTitleVisibility = () => {
356
- const titleElement = canvasElement.querySelector('.map-title')
357
- const classes = titleElement ? Array.from(titleElement.classList) : []
359
+ const titleElement = canvasElement.querySelector('.cove-title, header.cove-component__header')
358
360
  return {
359
- isVisible: classes.includes('visible'),
360
- isHidden: classes.includes('hidden')
361
+ isPresent: Boolean(titleElement)
361
362
  }
362
363
  }
363
364
 
364
- // Test config has showTitle: true, so title starts visible
365
+ // Test config has showTitle: true, so title starts visible (present in DOM)
365
366
  await performAndAssert(
366
367
  'Show Title → Hide',
367
368
  getTitleVisibility,
368
369
  async () => {
369
370
  await userEvent.click(showTitleCheckbox)
370
371
  },
371
- (before, after) => before.isVisible && !after.isVisible && after.isHidden
372
+ (before, after) => before.isPresent && !after.isPresent
372
373
  )
373
374
 
374
375
  await performAndAssert(
@@ -377,7 +378,7 @@ export const GeneralSectionTests: Story = {
377
378
  async () => {
378
379
  await userEvent.click(showTitleCheckbox)
379
380
  },
380
- (before, after) => !before.isVisible && after.isVisible && !after.isHidden
381
+ (before, after) => !before.isPresent && after.isPresent
381
382
  )
382
383
 
383
384
  // ==========================================================================
@@ -388,7 +389,7 @@ export const GeneralSectionTests: Story = {
388
389
  expect(superTitleInput).toBeTruthy()
389
390
 
390
391
  const getSuperTitleVisual = () => {
391
- const titleElement = canvasElement.querySelector('.map-title')
392
+ const titleElement = canvasElement.querySelector('.cove-title, .map-title')
392
393
  return {
393
394
  titleText: titleElement?.textContent || ''
394
395
  }
@@ -404,6 +405,60 @@ export const GeneralSectionTests: Story = {
404
405
  (before, after) => !before.titleText.includes('Super Title Text') && after.titleText.includes('Super Title Text')
405
406
  )
406
407
 
408
+ // ==========================================================================
409
+ // TEST: Title Style dropdown
410
+ // Verifies: Changing title style changes the heading element (h2/h3) used
411
+ // ==========================================================================
412
+ const titleStyleSelect = canvas.getByLabelText(/Title Style/i) as HTMLSelectElement
413
+ expect(titleStyleSelect).toBeTruthy()
414
+
415
+ const getTitleStyleVisual = () => {
416
+ const coveTitleElement = canvasElement.querySelector('.cove-title')
417
+ const legacyTitleElement = canvasElement.querySelector('header.cove-component__header')
418
+
419
+ // For modern titles, check for h2 (large) or h3 (small) elements
420
+ const hasH2 = Boolean(coveTitleElement?.querySelector('h2'))
421
+ const hasH3 = Boolean(coveTitleElement?.querySelector('h3'))
422
+
423
+ return {
424
+ hasCoveTitle: Boolean(coveTitleElement),
425
+ hasLegacyTitle: Boolean(legacyTitleElement),
426
+ isSmall: hasH3,
427
+ isLarge: hasH2
428
+ }
429
+ }
430
+
431
+ // Current config has titleStyle: 'small'
432
+ // Test: Change to 'large'
433
+ await performAndAssert(
434
+ 'Title Style → Change to Large',
435
+ getTitleStyleVisual,
436
+ async () => {
437
+ await userEvent.selectOptions(titleStyleSelect, 'large')
438
+ },
439
+ (before, after) => before.isSmall && after.isLarge && after.hasCoveTitle && !after.hasLegacyTitle
440
+ )
441
+
442
+ // Test: Change to 'legacy'
443
+ await performAndAssert(
444
+ 'Title Style → Change to Legacy',
445
+ getTitleStyleVisual,
446
+ async () => {
447
+ await userEvent.selectOptions(titleStyleSelect, 'legacy')
448
+ },
449
+ (before, after) => before.hasCoveTitle && !after.hasCoveTitle && after.hasLegacyTitle
450
+ )
451
+
452
+ // Test: Change back to 'small'
453
+ await performAndAssert(
454
+ 'Title Style → Change back to Small',
455
+ getTitleStyleVisual,
456
+ async () => {
457
+ await userEvent.selectOptions(titleStyleSelect, 'small')
458
+ },
459
+ (before, after) => before.hasLegacyTitle && !after.hasLegacyTitle && after.isSmall && after.hasCoveTitle
460
+ )
461
+
407
462
  // ==========================================================================
408
463
  // TEST: Message/Intro Text field
409
464
  // Verifies: Intro text appears in section with class 'introText'
@@ -1573,10 +1628,8 @@ export const FiltersSectionTests: Story = {
1573
1628
  await performAndAssert(
1574
1629
  'Add Filter → Click button',
1575
1630
  () => {
1576
- const filtersList = canvasElement.querySelector('.filters-list')
1577
- // Count all filter items (both collapsed and expanded)
1578
- const allFilterItems = Array.from(filtersList?.querySelectorAll('li, .edit-block, .mb-1') || [])
1579
- const collapsedFilters = filtersList?.querySelectorAll('.mb-1:has(button)') || []
1631
+ const filtersList = canvasElement.querySelector('.draggable-field-list')
1632
+ const collapsedFilters = filtersList?.querySelectorAll('.editor-field-item') || []
1580
1633
  return {
1581
1634
  hasFiltersList: Boolean(filtersList),
1582
1635
  hasCollapsedFilter: collapsedFilters.length > 0
@@ -1591,16 +1644,16 @@ export const FiltersSectionTests: Story = {
1591
1644
  }
1592
1645
  )
1593
1646
 
1594
- // Find and expand the collapsed filter
1595
- const filtersList = canvasElement.querySelector('.filters-list')
1596
- const expandButton = filtersList?.querySelector('.mb-1 button') as HTMLButtonElement
1647
+ // Find and expand the collapsed filter (click the header expand button)
1648
+ const filtersList = canvasElement.querySelector('.draggable-field-list')
1649
+ const expandButton = filtersList?.querySelector('.editor-field-item__header button') as HTMLButtonElement
1597
1650
  await userEvent.click(expandButton)
1598
1651
 
1599
- // Wait for the expanded filter block
1600
- await waitForPresence('.filters-list .edit-block', canvasElement)
1652
+ // Wait for the expanded filter content
1653
+ await waitForPresence('.draggable-field-list .editor-field-item__content', canvasElement)
1601
1654
 
1602
- // Find the newly added filter section
1603
- const filterBlock = filtersList?.querySelector('.edit-block') as HTMLElement
1655
+ // Find the newly added filter section content
1656
+ const filterBlock = filtersList?.querySelector('.editor-field-item__content') as HTMLElement
1604
1657
 
1605
1658
  // ==========================================================================
1606
1659
  // TEST: Select STATE as the filter column
@@ -1612,7 +1665,7 @@ export const FiltersSectionTests: Story = {
1612
1665
  }) as HTMLSelectElement
1613
1666
 
1614
1667
  const getDefaultValueState = () => {
1615
- const updatedFilterBlock = filtersList?.querySelector('.edit-block') as HTMLElement
1668
+ const updatedFilterBlock = filtersList?.querySelector('.editor-field-item__content') as HTMLElement
1616
1669
  const defaultValueSelect = Array.from(updatedFilterBlock?.querySelectorAll('select') || []).find(select => {
1617
1670
  const label = select.closest('label')
1618
1671
  const labelSpan = label?.querySelector('.edit-label')
@@ -1668,7 +1721,7 @@ export const FiltersSectionTests: Story = {
1668
1721
  // ==========================================================================
1669
1722
  // TEST: Select Alabama as the default filter value
1670
1723
  // ==========================================================================
1671
- const updatedFilterBlock = filtersList?.querySelector('.edit-block') as HTMLElement
1724
+ const updatedFilterBlock = filtersList?.querySelector('.editor-field-item__content') as HTMLElement
1672
1725
  const defaultValueSelect = Array.from(updatedFilterBlock?.querySelectorAll('select') || []).find(select => {
1673
1726
  const label = select.closest('label')
1674
1727
  const labelSpan = label?.querySelector('.edit-label')
@@ -2091,9 +2144,13 @@ export const DataTableSectionTests: Story = {
2091
2144
  await performAndAssert(
2092
2145
  'Enable Image Download → Enable button',
2093
2146
  () => {
2094
- const downloadImgButton = Array.from(canvasElement.querySelectorAll('button')).find(
2095
- btn => btn.textContent?.includes('Download Image') || btn.classList.contains('download-image')
2096
- )
2147
+ const downloadImgButton =
2148
+ Array.from(canvasElement.querySelectorAll('button')).find(
2149
+ btn => btn.textContent?.includes('Download Image') || btn.classList.contains('download-image')
2150
+ ) ||
2151
+ Array.from(canvasElement.querySelectorAll('a[role="button"]')).find(
2152
+ link => link.textContent?.includes('Download Map') && link.textContent?.includes('PNG')
2153
+ )
2097
2154
  return {
2098
2155
  hasDownloadImgButton: Boolean(downloadImgButton)
2099
2156
  }
@@ -2163,36 +2220,34 @@ export const VisualSectionTests: StoryObj<typeof CdcMap> = {
2163
2220
  await performAndAssert(
2164
2221
  'Show Title → Toggle off',
2165
2222
  () => {
2166
- const title = canvasElement.querySelector('.map-title')
2167
- const isVisible = title?.classList.contains('visible')
2223
+ const titleElement = canvasElement.querySelector('.cove-title, header.cove-component__header')
2168
2224
  return {
2169
- isVisible: Boolean(isVisible)
2225
+ isPresent: Boolean(titleElement)
2170
2226
  }
2171
2227
  },
2172
2228
  async () => {
2173
2229
  await userEvent.click(showTitleCheckbox)
2174
2230
  },
2175
2231
  (before, after) => {
2176
- // After toggling off, title should be hidden
2177
- return before.isVisible && !after.isVisible
2232
+ // After toggling off, title should be removed from DOM
2233
+ return before.isPresent && !after.isPresent
2178
2234
  }
2179
2235
  )
2180
2236
 
2181
2237
  await performAndAssert(
2182
2238
  'Show Title → Toggle back on',
2183
2239
  () => {
2184
- const title = canvasElement.querySelector('.map-title')
2185
- const isVisible = title?.classList.contains('visible')
2240
+ const titleElement = canvasElement.querySelector('.cove-title, header.cove-component__header')
2186
2241
  return {
2187
- isVisible: Boolean(isVisible)
2242
+ isPresent: Boolean(titleElement)
2188
2243
  }
2189
2244
  },
2190
2245
  async () => {
2191
2246
  await userEvent.click(showTitleCheckbox)
2192
2247
  },
2193
2248
  (before, after) => {
2194
- // After toggling back on, title should be visible
2195
- return !before.isVisible && after.isVisible
2249
+ // After toggling back on, title should be present in DOM
2250
+ return !before.isPresent && after.isPresent
2196
2251
  }
2197
2252
  )
2198
2253
 
@@ -1,4 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { within, expect } from 'storybook/test'
2
3
  import CdcMap from '../CdcMap'
3
4
  import EqualNumberOptInExample from './_mock/DEV-7286.json'
4
5
  import EqualNumberMap from './_mock/equal-number.json'
@@ -11,6 +12,11 @@ import USBubbleCities from './_mock/us-bubble-cities.json'
11
12
  import { editConfigKeys } from '@cdc/core/helpers/configHelpers'
12
13
  import exampleLegendBins from './_mock/legend-bins.json'
13
14
 
15
+ // Fallback step function for test descriptions
16
+ const step = async (description: string, fn: () => Promise<void> | void) => {
17
+ await fn()
18
+ }
19
+
14
20
  const meta: Meta<typeof CdcMap> = {
15
21
  title: 'Components/Templates/Map',
16
22
  component: CdcMap
@@ -18,10 +24,33 @@ const meta: Meta<typeof CdcMap> = {
18
24
 
19
25
  type Story = StoryObj<typeof CdcMap>
20
26
 
27
+ // Helper function to test map rendering
28
+ const testMapRendering = async (canvasElement: HTMLElement, storyName: string) => {
29
+ const canvas = within(canvasElement)
30
+
31
+ await step('Wait for map to render', async () => {
32
+ const mapElement = await canvas.findByRole('img', { hidden: true }, { timeout: 10000 })
33
+ expect(mapElement).toBeInTheDocument()
34
+ })
35
+
36
+ await step('Verify SVG element is present', async () => {
37
+ const svgElement = canvasElement.querySelector('svg')
38
+ expect(svgElement).toBeInTheDocument()
39
+ })
40
+
41
+ await step('Verify COVE module wrapper is present', async () => {
42
+ const coveModule = canvasElement.querySelector('.cdc-open-viz-module')
43
+ expect(coveModule).toBeInTheDocument()
44
+ })
45
+ }
46
+
21
47
  export const Equal_Interval_Map: Story = {
22
48
  args: {
23
49
  isEditor: true,
24
50
  configUrl: 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/page-elements/equal-interval-map.json'
51
+ },
52
+ play: async ({ canvasElement }) => {
53
+ await testMapRendering(canvasElement, 'Equal Interval Map')
25
54
  }
26
55
  }
27
56
 
@@ -43,41 +72,62 @@ export const Scale_Based: Story = {
43
72
  configUrl:
44
73
  'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/examples/Scale-Based-Categorical-Map-With-Special-Classes.json',
45
74
  isEditor: true
75
+ },
76
+ play: async ({ canvasElement }) => {
77
+ await testMapRendering(canvasElement, 'Scale Based')
46
78
  }
47
79
  }
48
80
  export const Qualitative: Story = {
49
81
  args: {
50
82
  configUrl: 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/page-elements/qualitative-map.json'
83
+ },
84
+ play: async ({ canvasElement }) => {
85
+ await testMapRendering(canvasElement, 'Qualitative')
51
86
  }
52
87
  }
53
88
 
54
89
  export const World_Map: Story = {
55
90
  args: {
56
91
  configUrl: 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/examples/world-data-map-example.json'
92
+ },
93
+ play: async ({ canvasElement }) => {
94
+ await testMapRendering(canvasElement, 'World Map')
57
95
  }
58
96
  }
59
97
 
60
98
  export const Filterable_Map: Story = {
61
99
  args: {
62
100
  configUrl: 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/page-elements/gender-rate-map.json'
101
+ },
102
+ play: async ({ canvasElement }) => {
103
+ await testMapRendering(canvasElement, 'Filterable Map')
63
104
  }
64
105
  }
65
106
 
66
107
  export const Hex_Map: Story = {
67
108
  args: {
68
109
  configUrl: 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/examples/Hex_Map.json'
110
+ },
111
+ play: async ({ canvasElement }) => {
112
+ await testMapRendering(canvasElement, 'Hex Map')
69
113
  }
70
114
  }
71
115
 
72
116
  export const County_Map: Story = {
73
117
  args: {
74
118
  configUrl: 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/examples/US-County-Level-Map.json'
119
+ },
120
+ play: async ({ canvasElement }) => {
121
+ await testMapRendering(canvasElement, 'County Map')
75
122
  }
76
123
  }
77
124
 
78
125
  export const Single_State: Story = {
79
126
  args: {
80
127
  configUrl: 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/examples/example-data-map-counties.json'
128
+ },
129
+ play: async ({ canvasElement }) => {
130
+ await testMapRendering(canvasElement, 'Single State')
81
131
  }
82
132
  }
83
133
 
@@ -102,18 +152,27 @@ export const Multi_Country_Hide_Mode: Story = {
102
152
  export const Bubble_Map: Story = {
103
153
  args: {
104
154
  configUrl: 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/examples/example-Bubble-Map-world.json'
155
+ },
156
+ play: async ({ canvasElement }) => {
157
+ await testMapRendering(canvasElement, 'Bubble Map')
105
158
  }
106
159
  }
107
160
 
108
161
  export const HHS_Region_Map: Story = {
109
162
  args: {
110
163
  configUrl: 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/examples/example-hhs-regions-data.json'
164
+ },
165
+ play: async ({ canvasElement }) => {
166
+ await testMapRendering(canvasElement, 'HHS Region Map')
111
167
  }
112
168
  }
113
169
 
114
170
  export const Custom_Map_Layers: Story = {
115
171
  args: {
116
172
  configUrl: 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/examples/custom-layer-map.json'
173
+ },
174
+ play: async ({ canvasElement }) => {
175
+ await testMapRendering(canvasElement, 'Custom Map Layers')
117
176
  }
118
177
  }
119
178
 
@@ -199,4 +258,39 @@ export const US_Bubble_Cities_Test: Story = {
199
258
  }
200
259
  }
201
260
 
261
+ export const City_Styles_By_Variable: Story = {
262
+ args: {
263
+ config: editConfigKeys(exampleCityState, [
264
+ {
265
+ path: ['visual', 'additionalCityStyles'],
266
+ value: [
267
+ {
268
+ label: 'High Risk (Rate > 20)',
269
+ column: 'Rate',
270
+ value: '22',
271
+ shape: 'Star'
272
+ },
273
+ {
274
+ label: 'School Location',
275
+ column: 'Location',
276
+ value: 'School',
277
+ shape: 'Triangle'
278
+ },
279
+ {
280
+ label: 'Vehicle Location',
281
+ column: 'Location',
282
+ value: 'Vehicle',
283
+ shape: 'Diamond'
284
+ }
285
+ ]
286
+ },
287
+ {
288
+ path: ['visual', 'cityStyle'],
289
+ value: 'circle'
290
+ }
291
+ ]),
292
+ isEditor: true
293
+ }
294
+ }
295
+
202
296
  export default meta
@@ -8,6 +8,7 @@
8
8
  "geoBorderColor": "darkGray",
9
9
  "headerColor": "theme-blue",
10
10
  "title": "",
11
+ "titleStyle": "small",
11
12
  "showTitle": true,
12
13
  "showSidebar": true,
13
14
  "showDownloadButton": true,