@cdc/map 4.26.4 → 4.26.5

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/CONFIG.md +70 -37
  2. package/LICENSE +201 -0
  3. package/README.md +6 -2
  4. package/dist/cdcmap.js +23502 -22964
  5. package/examples/default-county.json +3 -0
  6. package/examples/minimal-example.json +6 -2
  7. package/package.json +3 -3
  8. package/src/CdcMapComponent.tsx +13 -3
  9. package/src/_stories/CdcMap.AltText.stories.tsx +122 -0
  10. package/src/_stories/CdcMap.Editor.ColumnsSectionTests.stories.tsx +15 -16
  11. package/src/_stories/CdcMap.FocusVisibility.stories.tsx +87 -0
  12. package/src/_stories/CdcMap.HiddenMount.stories.tsx +69 -0
  13. package/src/_stories/CdcMap.ResetBehavior.stories.tsx +32 -0
  14. package/src/_stories/CdcMap.Zoom.stories.tsx +111 -0
  15. package/src/_stories/CdcMap.smoke.stories.tsx +48 -0
  16. package/src/_stories/_mock/alt_text_metadata.json +65 -0
  17. package/src/_stories/_mock/world-bubble-reset.json +138 -0
  18. package/src/_stories/_mock/world-data-zoom-filters.json +166 -0
  19. package/src/components/BubbleList.tsx +13 -0
  20. package/src/components/EditorPanel/components/EditorPanel.tsx +134 -0
  21. package/src/components/FilterControls.tsx +21 -0
  22. package/src/components/SmallMultiples/SmallMultiples.tsx +2 -2
  23. package/src/components/UsaMap/components/UsaMap.County.tsx +39 -9
  24. package/src/components/UsaMap/components/UsaMap.Region.tsx +5 -2
  25. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +33 -10
  26. package/src/components/UsaMap/components/UsaMap.State.tsx +9 -2
  27. package/src/components/WorldMap/WorldMap.tsx +37 -4
  28. package/src/components/ZoomableGroup.tsx +23 -3
  29. package/src/components/filterControls.styles.css +6 -0
  30. package/src/data/initial-state.js +2 -0
  31. package/src/helpers/countyTerritories.ts +1 -1
  32. package/src/helpers/generateRuntimeFilters.ts +2 -1
  33. package/src/helpers/handleMapAriaLabels.ts +45 -30
  34. package/src/helpers/shouldAutoResetSingleStateZoom.ts +22 -0
  35. package/src/helpers/tests/handleMapAriaLabels.test.ts +71 -0
  36. package/src/helpers/tests/shouldAutoResetSingleStateZoom.test.ts +71 -0
  37. package/src/hooks/useGeoClickHandler.ts +13 -1
  38. package/src/hooks/useStateZoom.tsx +39 -20
  39. package/src/hooks/useTooltip.test.tsx +2 -16
  40. package/src/index.jsx +5 -2
  41. package/src/scss/main.scss +6 -21
  42. package/src/scss/map.scss +20 -0
  43. package/src/types/MapConfig.ts +5 -0
  44. package/src/types/MapContext.ts +3 -0
@@ -97,6 +97,9 @@
97
97
  "runtime": {
98
98
  "editorErrorMessage": []
99
99
  },
100
+ "migrations": {
101
+ "showPuertoRico": false
102
+ },
100
103
  "visual": {
101
104
  "minBubbleSize": 1,
102
105
  "maxBubbleSize": 20,
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "version": "4.26.4",
3
- "color": "pinkpurple",
4
3
  "general": {
5
4
  "title": "Minimal US Map",
6
5
  "geoType": "us",
7
6
  "type": "data",
8
- "showTitle": true
7
+ "showTitle": true,
8
+ "palette": {
9
+ "name": "sequential_blue",
10
+ "version": "2.0",
11
+ "isReversed": false
12
+ }
9
13
  },
10
14
  "type": "map",
11
15
  "columns": {
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@cdc/map",
3
- "version": "4.26.4",
3
+ "version": "4.26.5",
4
4
  "description": "React component for visualizing tabular data on a map of the United States or the world.",
5
5
  "license": "Apache-2.0",
6
6
  "bugs": "https://github.com/CDCgov/cdc-open-viz/issues",
7
7
  "dependencies": {
8
- "@cdc/core": "^4.26.4",
8
+ "@cdc/core": "^4.26.5",
9
9
  "@googlemaps/markerclusterer": "^2.5.3",
10
10
  "@hello-pangea/dnd": "^16.2.0",
11
11
  "@react-google-maps/api": "^2.20.8",
@@ -42,7 +42,7 @@
42
42
  "vite-plugin-svgr": "^4.2.0",
43
43
  "whatwg-fetch": "^3.6.20"
44
44
  },
45
- "gitHead": "432a2d1acab22915fafe793cb9da1f10318ff793",
45
+ "gitHead": "61c025165d96b45a6002c34582c5a622a9d865a9",
46
46
  "main": "dist/cdcmap",
47
47
  "moduleName": "CdcMap",
48
48
  "peerDependencies": {
@@ -35,6 +35,7 @@ import {
35
35
  import { generateRuntimeFilters } from './helpers/generateRuntimeFilters'
36
36
  import { type MapReducerType, MapState } from './store/map.reducer'
37
37
  import { addValuesToFilters } from '@cdc/core/helpers/addValuesToFilters'
38
+ import { hasVisibleVizFilters } from '@cdc/core/helpers/filterVisibility'
38
39
  import { processMarkupVariables } from '@cdc/core/helpers/markupProcessor'
39
40
 
40
41
  // Map Helpers
@@ -94,6 +95,8 @@ type CdcMapComponent = {
94
95
  logo?: string
95
96
  navigationHandler: Function
96
97
  setSharedFilter: Function
98
+ clearSharedFilter?: (key: string) => void
99
+ hasActiveSharedFilter?: boolean
97
100
  setSharedFilterValue: Function
98
101
  setConfig?: Function
99
102
  loadConfig?: Function
@@ -108,6 +111,8 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
108
111
  isEditor = false,
109
112
  logo = '',
110
113
  setSharedFilter,
114
+ clearSharedFilter,
115
+ hasActiveSharedFilter = false,
111
116
  setSharedFilterValue,
112
117
  link,
113
118
  setConfig: setParentConfig,
@@ -433,6 +438,8 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
433
438
  setConfig,
434
439
  setFilteredStateCountyCode,
435
440
  setSharedFilter,
441
+ clearSharedFilter,
442
+ hasActiveSharedFilter,
436
443
  setSharedFilterValue,
437
444
  config,
438
445
  statesToShow,
@@ -532,6 +539,8 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
532
539
  return config
533
540
  }
534
541
 
542
+ const filterConfig = applyStateFilter(config)
543
+
535
544
  return (
536
545
  <LegendMemoProvider legendMemo={legendMemo} legendSpecialClassLastMemo={legendSpecialClassLastMemo}>
537
546
  <ConfigContext.Provider value={mapProps}>
@@ -556,7 +565,7 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
556
565
  ]
557
566
  .filter(Boolean)
558
567
  .join(' ')}
559
- innerProps={{ 'aria-label': 'Map: ' + title, ref: innerContainerRef }}
568
+ innerProps={{ ref: innerContainerRef }}
560
569
  bodyWrapClassName={isTp5Treatment ? 'cdc-callout d-flex flex-column' : ''}
561
570
  bodyClassName={[
562
571
  !config.visual?.border || isTp5Treatment ? 'no-borders' : '',
@@ -570,9 +579,9 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
570
579
  .filter(Boolean)
571
580
  .join(' ')}
572
581
  filters={
573
- config?.filters?.length > 0 || config.general.showStateDropdown ? (
582
+ hasVisibleVizFilters(filterConfig.filters) ? (
574
583
  <Filters
575
- config={applyStateFilter(config)}
584
+ config={filterConfig}
576
585
  setFilters={setFilters}
577
586
  dimensions={dimensions}
578
587
  interactionLabel={interactionLabel}
@@ -746,6 +755,7 @@ const CdcMapComponent: React.FC<CdcMapComponent> = ({
746
755
  enableMarkupVariables={config.enableMarkupVariables}
747
756
  data={config.data}
748
757
  dataMetadata={config.dataMetadata}
758
+ footerClassName='cove-visualization__footnotes'
749
759
  />
750
760
  </VisualizationContainer>
751
761
  </MapDispatchContext.Provider>
@@ -0,0 +1,122 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { within, expect } from 'storybook/test'
3
+ import CdcMap from '../CdcMap'
4
+ import { assertVisualizationRendered, waitForPresence, waitForEditor, openAccordion } from '@cdc/core/helpers/testing'
5
+ import altTextConfig from './_mock/alt_text_metadata.json'
6
+
7
+ const meta: Meta<typeof CdcMap> = {
8
+ title: 'Components/Templates/Map/Accessible Alt Text',
9
+ component: CdcMap,
10
+ parameters: {
11
+ docs: {
12
+ description: {
13
+ component:
14
+ 'Demonstrates SVG accessibility for maps: auto-generated aria-label with optional configurable description concatenated into the label for reliable screen reader support.'
15
+ }
16
+ }
17
+ }
18
+ }
19
+
20
+ export default meta
21
+ type Story = StoryObj<typeof CdcMap>
22
+
23
+ export const MetadataDescription: Story = {
24
+ args: {
25
+ config: {
26
+ ...altTextConfig,
27
+ altText: { type: 'metadata', metadataKey: 'altDescription' }
28
+ },
29
+ isEditor: false
30
+ },
31
+ parameters: {
32
+ docs: {
33
+ description: {
34
+ story:
35
+ 'Description pulled from dataMetadata is concatenated into the aria-label after the auto-generated title for reliable screen reader support.'
36
+ }
37
+ }
38
+ },
39
+ play: async ({ canvasElement }) => {
40
+ await assertVisualizationRendered(canvasElement)
41
+ const svg = await waitForPresence('svg[role="img"]', canvasElement)
42
+
43
+ const expected = `United States map with the title: COVID-19 Case Rates by State. ${altTextConfig.dataMetadata.altDescription}`
44
+ expect(svg?.getAttribute('aria-label')).toBe(expected)
45
+ }
46
+ }
47
+
48
+ export const StaticDescription: Story = {
49
+ args: {
50
+ config: {
51
+ ...altTextConfig,
52
+ altText: {
53
+ type: 'static',
54
+ value: 'US map showing COVID-19 rates concentrated in the Southeast region.'
55
+ }
56
+ },
57
+ isEditor: false
58
+ },
59
+ parameters: {
60
+ docs: {
61
+ description: {
62
+ story: 'Static manually written description concatenated with the auto-generated title.'
63
+ }
64
+ }
65
+ },
66
+ play: async ({ canvasElement }) => {
67
+ await assertVisualizationRendered(canvasElement)
68
+ const svg = await waitForPresence('svg[role="img"]', canvasElement)
69
+
70
+ expect(svg?.getAttribute('aria-label')).toBe(
71
+ 'United States map with the title: COVID-19 Case Rates by State. US map showing COVID-19 rates concentrated in the Southeast region.'
72
+ )
73
+ }
74
+ }
75
+
76
+ export const AutoGenerated: Story = {
77
+ args: {
78
+ config: altTextConfig,
79
+ isEditor: false
80
+ },
81
+ parameters: {
82
+ docs: {
83
+ description: {
84
+ story:
85
+ 'No altText configured. Falls back to the auto-generated aria-label: "United States map with the title: COVID-19 Case Rates by State".'
86
+ }
87
+ }
88
+ },
89
+ play: async ({ canvasElement }) => {
90
+ await assertVisualizationRendered(canvasElement)
91
+ const svg = await waitForPresence('svg[role="img"]', canvasElement)
92
+
93
+ expect(svg?.getAttribute('aria-label')).toBe('United States map with the title: COVID-19 Case Rates by State')
94
+ }
95
+ }
96
+
97
+ export const EditorWithMetadata: Story = {
98
+ args: {
99
+ config: {
100
+ ...altTextConfig,
101
+ altText: { type: 'metadata', metadataKey: 'altDescription' }
102
+ },
103
+ isEditor: true
104
+ },
105
+ parameters: {
106
+ docs: {
107
+ description: {
108
+ story:
109
+ 'Editor mode showing the alt text description control in the General accordion with metadata-driven description.'
110
+ }
111
+ }
112
+ },
113
+ play: async ({ canvasElement }) => {
114
+ const canvas = within(canvasElement)
115
+ await waitForEditor(canvas)
116
+
117
+ await openAccordion(canvas, 'General')
118
+
119
+ const descPreview = await waitForPresence('[data-testid="alt-text-desc-preview"]', canvasElement)
120
+ expect(descPreview?.textContent).toContain('Choropleth map of the United States')
121
+ }
122
+ }
@@ -433,13 +433,13 @@ export const ColumnsSectionTests: Story = {
433
433
  const tableHeaders = Array.from(dataTable?.querySelectorAll('th') || [])
434
434
  const tableHeaderText = tableHeaders.map(th => th.textContent?.trim() || '')
435
435
 
436
- // Check if "Rate" column header appears in data table
437
- const hasRateColumn = tableHeaderText.some(text => text.includes('Rate') || text.includes('Value'))
436
+ // The primary map column renders as "Location" in this story's data table.
437
+ const hasPrimaryColumn = tableHeaderText.some(text => text.includes('Location'))
438
438
 
439
439
  return {
440
440
  hasDataTable: Boolean(dataTable),
441
441
  tableHeaders: tableHeaderText,
442
- hasRateColumn: hasRateColumn
442
+ hasPrimaryColumn: hasPrimaryColumn
443
443
  }
444
444
  }
445
445
 
@@ -451,8 +451,8 @@ export const ColumnsSectionTests: Story = {
451
451
  await userEvent.click(showInTableCheckbox)
452
452
  },
453
453
  (before, after) => {
454
- // Rate column should disappear from data table headers
455
- return before.hasRateColumn && !after.hasRateColumn
454
+ // Primary column should disappear from data table headers.
455
+ return before.hasPrimaryColumn && !after.hasPrimaryColumn
456
456
  }
457
457
  )
458
458
 
@@ -463,8 +463,8 @@ export const ColumnsSectionTests: Story = {
463
463
  await userEvent.click(showInTableCheckbox)
464
464
  },
465
465
  (before, after) => {
466
- // Rate column should reappear in data table headers
467
- return !before.hasRateColumn && after.hasRateColumn
466
+ // Primary column should reappear in data table headers.
467
+ return !before.hasPrimaryColumn && after.hasPrimaryColumn
468
468
  }
469
469
  )
470
470
 
@@ -482,14 +482,13 @@ export const ColumnsSectionTests: Story = {
482
482
  const geoGroup = canvasElement.querySelector('g.geo-group') as SVGGElement
483
483
  const tooltipHtml = geoGroup?.getAttribute('data-tooltip-html') || ''
484
484
 
485
- // Check if tooltip contains data values (numbers with potential $ and % from earlier tests)
486
- // Look for patterns like list items with numeric values
487
- const hasDataValues = tooltipHtml.includes('<li') && /\$?\d+\.\d{2}%?/.test(tooltipHtml)
485
+ const hasPrimaryLocationField = tooltipHtml.includes('Location:')
486
+ const hasValueField = tooltipHtml.includes('Value:')
488
487
 
489
488
  return {
490
489
  tooltipContent: tooltipHtml,
491
- hasDataValues: hasDataValues,
492
- tooltipLength: tooltipHtml.length
490
+ hasPrimaryLocationField: hasPrimaryLocationField,
491
+ hasValueField: hasValueField
493
492
  }
494
493
  }
495
494
 
@@ -501,8 +500,8 @@ export const ColumnsSectionTests: Story = {
501
500
  await userEvent.click(showInTooltipsCheckbox)
502
501
  },
503
502
  (before, after) => {
504
- // Tooltip should no longer contain data values, should be shorter
505
- return before.hasDataValues && !after.hasDataValues && after.tooltipLength < before.tooltipLength
503
+ // Primary tooltip field should be enabled.
504
+ return !before.hasPrimaryLocationField && after.hasPrimaryLocationField && after.hasValueField
506
505
  }
507
506
  )
508
507
 
@@ -513,8 +512,8 @@ export const ColumnsSectionTests: Story = {
513
512
  await userEvent.click(showInTooltipsCheckbox)
514
513
  },
515
514
  (before, after) => {
516
- // Tooltip should contain data values again, should be longer
517
- return !before.hasDataValues && after.hasDataValues && after.tooltipLength > before.tooltipLength
515
+ // Primary tooltip field should be disabled again.
516
+ return before.hasPrimaryLocationField && !after.hasPrimaryLocationField && after.hasValueField
518
517
  }
519
518
  )
520
519
 
@@ -0,0 +1,87 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { within, expect, userEvent } from 'storybook/test'
3
+ import CdcMap from '../CdcMap'
4
+ import { assertVisualizationRendered, waitForPresence } from '@cdc/core/helpers/testing'
5
+ import singleStateConfig from './_mock/DEV-8942.json'
6
+ import worldBubbleReset from './_mock/world-bubble-reset.json'
7
+
8
+ const meta: Meta<typeof CdcMap> = {
9
+ title: 'Components/Templates/Map/Focus Visibility',
10
+ component: CdcMap,
11
+ parameters: {
12
+ docs: {
13
+ description: {
14
+ component:
15
+ 'Regression coverage for map focus treatment so single-state maps suppress pointer-only outlines while keeping keyboard-visible focus.'
16
+ }
17
+ }
18
+ }
19
+ }
20
+
21
+ export default meta
22
+
23
+ type Story = StoryObj<typeof CdcMap>
24
+
25
+ export const SingleStateFocusVisibility: Story = {
26
+ args: {
27
+ config: singleStateConfig,
28
+ isEditor: false
29
+ },
30
+ play: async ({ canvasElement }) => {
31
+ await assertVisualizationRendered(canvasElement)
32
+ await waitForPresence('path.county', canvasElement)
33
+
34
+ const mapRegion = canvasElement.querySelector('.map-container[role="region"]') as HTMLElement
35
+ const geographyContainer = canvasElement.querySelector('.geography-container') as HTMLElement
36
+ const countyPath = canvasElement.querySelector('path.county') as SVGPathElement
37
+
38
+ expect(mapRegion).toBeTruthy()
39
+ expect(geographyContainer).toBeTruthy()
40
+ expect(countyPath).toBeTruthy()
41
+
42
+ await userEvent.click(countyPath)
43
+ expect(geographyContainer.matches(':focus-visible')).toBe(false)
44
+ expect(geographyContainer).not.toHaveFocus()
45
+ expect(mapRegion.matches(':focus-visible')).toBe(false)
46
+ expect(mapRegion).not.toHaveFocus()
47
+
48
+ await userEvent.tab()
49
+
50
+ const focusedElement = document.activeElement as HTMLElement | null
51
+ expect(focusedElement).toBeTruthy()
52
+ expect(canvasElement.contains(focusedElement)).toBe(true)
53
+ expect(focusedElement?.matches(':focus-visible')).toBe(true)
54
+ }
55
+ }
56
+
57
+ export const WorldBubbleFocusVisibility: Story = {
58
+ args: {
59
+ config: worldBubbleReset,
60
+ isEditor: false
61
+ },
62
+ play: async ({ canvasElement }) => {
63
+ await assertVisualizationRendered(canvasElement)
64
+ await waitForPresence('circle.bubble.country--France', canvasElement)
65
+
66
+ const mapRegion = canvasElement.querySelector('.map-container[role="region"]') as HTMLElement
67
+ const geographyContainer = canvasElement.querySelector('.geography-container') as HTMLElement
68
+ const bubble = canvasElement.querySelector('circle.bubble.country--France') as SVGCircleElement
69
+
70
+ expect(mapRegion).toBeTruthy()
71
+ expect(geographyContainer).toBeTruthy()
72
+ expect(bubble).toBeTruthy()
73
+
74
+ await userEvent.click(bubble)
75
+ expect(bubble.matches(':focus-visible')).toBe(false)
76
+ expect(bubble).not.toHaveFocus()
77
+ expect(geographyContainer.matches(':focus-visible')).toBe(false)
78
+ expect(mapRegion.matches(':focus-visible')).toBe(false)
79
+
80
+ await userEvent.tab()
81
+
82
+ const focusedElement = document.activeElement as HTMLElement | null
83
+ expect(focusedElement).toBeTruthy()
84
+ expect(canvasElement.contains(focusedElement)).toBe(true)
85
+ expect(focusedElement?.matches(':focus-visible')).toBe(true)
86
+ }
87
+ }
@@ -0,0 +1,69 @@
1
+ import React, { useState } from 'react'
2
+ import type { Meta, StoryObj } from '@storybook/react-vite'
3
+ import CdcMap from '../CdcMap'
4
+ import CountyPatterns from './_mock/county-patterns.json'
5
+ import { within, expect, userEvent } from 'storybook/test'
6
+
7
+ const meta: Meta<typeof CdcMap> = {
8
+ title: 'Components/Templates/Map/Hidden Mount',
9
+ component: CdcMap,
10
+ parameters: {
11
+ layout: 'fullscreen'
12
+ }
13
+ }
14
+
15
+ export default meta
16
+
17
+ type Story = StoryObj<typeof CdcMap>
18
+
19
+ const HiddenMountHarness = () => {
20
+ const [isVisible, setIsVisible] = useState(false)
21
+
22
+ return (
23
+ <div style={{ padding: 24 }}>
24
+ <div style={{ maxWidth: 960, marginBottom: 16 }}>
25
+ <p style={{ marginBottom: 12 }}>
26
+ This story mounts the county map while hidden, mimicking host-page CSS or JS that reveals it later.
27
+ </p>
28
+ <button type='button' onClick={() => setIsVisible(true)}>
29
+ Reveal county map
30
+ </button>
31
+ </div>
32
+
33
+ <div
34
+ data-testid='county-hidden-mount-wrapper'
35
+ style={{
36
+ display: isVisible ? 'block' : 'none',
37
+ width: 960
38
+ }}
39
+ >
40
+ <CdcMap config={CountyPatterns} />
41
+ </div>
42
+ </div>
43
+ )
44
+ }
45
+
46
+ export const County_Map_Revealed_After_Hidden_Mount: Story = {
47
+ render: () => <HiddenMountHarness />,
48
+ parameters: {
49
+ docs: {
50
+ description: {
51
+ story:
52
+ 'Demonstrates the original county-map hidden-mount scenario: the map is mounted while hidden and only shown after host-page interaction.'
53
+ }
54
+ }
55
+ },
56
+ play: async ({ canvasElement }) => {
57
+ const canvas = within(canvasElement)
58
+ const revealButton = canvas.getByRole('button', { name: 'Reveal county map' })
59
+
60
+ await userEvent.click(revealButton)
61
+
62
+ const renderedCanvas = await canvas.findByRole('img', { hidden: true }, { timeout: 10000 })
63
+ expect(renderedCanvas).toBeInTheDocument()
64
+
65
+ const mapCanvas = canvasElement.querySelector('canvas') as HTMLCanvasElement | null
66
+ expect(mapCanvas?.width).toBeGreaterThan(0)
67
+ expect(mapCanvas?.height).toBeGreaterThan(0)
68
+ }
69
+ }
@@ -0,0 +1,32 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import CdcMap from '../CdcMap'
3
+ import worldDataZoomFilters from './_mock/world-data-zoom-filters.json'
4
+ import { assertVisualizationRendered, waitForPresence } from '@cdc/core/helpers/testing'
5
+
6
+ const meta: Meta<typeof CdcMap> = {
7
+ title: 'Components/Templates/Map/Reset Behavior',
8
+ component: CdcMap,
9
+ parameters: {
10
+ docs: {
11
+ description: {
12
+ component:
13
+ 'Manual repro story for world data maps that combine active filters with zoom so reset behavior can be validated directly in Storybook.'
14
+ }
15
+ }
16
+ }
17
+ }
18
+
19
+ export default meta
20
+
21
+ type Story = StoryObj<typeof CdcMap>
22
+
23
+ export const WorldDataZoomWithFilters: Story = {
24
+ args: {
25
+ config: worldDataZoomFilters,
26
+ isEditor: true
27
+ },
28
+ play: async ({ canvasElement }) => {
29
+ await assertVisualizationRendered(canvasElement)
30
+ await waitForPresence('path[data-country-code]', canvasElement)
31
+ }
32
+ }
@@ -0,0 +1,111 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { within, userEvent, expect } from 'storybook/test'
3
+ import CdcMap from '../CdcMap'
4
+ import SingleStateWithFilters from './_mock/DEV-8942.json'
5
+ import MultiCountry from './_mock/multi-country.json'
6
+ import CountyPatterns from './_mock/county-patterns.json'
7
+ import { assertVisualizationRendered, performAndAssert, waitForPresence } from '@cdc/core/helpers/testing'
8
+
9
+ type Story = StoryObj<typeof CdcMap>
10
+
11
+ const meta: Meta<typeof CdcMap> = {
12
+ title: 'Components/Templates/Map/Zoom',
13
+ component: CdcMap,
14
+ parameters: {
15
+ layout: 'fullscreen'
16
+ }
17
+ }
18
+
19
+ export default meta
20
+
21
+ const readZoomState = (canvasElement: HTMLElement) => {
22
+ const zoomTarget = canvasElement.querySelector('[data-zoom-transform]') as HTMLElement | null
23
+
24
+ return {
25
+ transform: zoomTarget?.getAttribute('data-zoom-transform') || '',
26
+ scale: Number(zoomTarget?.getAttribute('data-zoom-scale') || '0')
27
+ }
28
+ }
29
+
30
+ const verifyBasicZoomCycle = async (canvasElement: HTMLElement) => {
31
+ const canvas = within(canvasElement)
32
+
33
+ await assertVisualizationRendered(canvasElement)
34
+ await waitForPresence('button[aria-label="Zoom In"]', canvasElement)
35
+
36
+ const zoomInButton = canvas.getByLabelText('Zoom In')
37
+
38
+ await performAndAssert(
39
+ 'Map zooms in',
40
+ () => readZoomState(canvasElement),
41
+ async () => {
42
+ await userEvent.click(zoomInButton)
43
+ },
44
+ (before, after) => before.transform !== after.transform && after.scale > before.scale
45
+ )
46
+
47
+ const resetButton = canvas.queryByRole('button', { name: 'Reset Zoom' })
48
+
49
+ if (resetButton) {
50
+ await performAndAssert(
51
+ 'Map reset returns zoom to default',
52
+ () => readZoomState(canvasElement),
53
+ async () => {
54
+ await userEvent.click(resetButton)
55
+ },
56
+ (before, after) => before.transform !== after.transform && after.scale === 1
57
+ )
58
+ } else {
59
+ expect(readZoomState(canvasElement).scale).toBeGreaterThan(1)
60
+ }
61
+ }
62
+
63
+ export const SingleState: Story = {
64
+ args: {
65
+ config: SingleStateWithFilters
66
+ },
67
+ parameters: {
68
+ docs: {
69
+ description: {
70
+ story: 'Single-state zoom baseline. Use this to confirm local zoom and reset behavior outside dashboards.'
71
+ }
72
+ }
73
+ },
74
+ play: async ({ canvasElement }) => {
75
+ await verifyBasicZoomCycle(canvasElement)
76
+ }
77
+ }
78
+
79
+ export const County: Story = {
80
+ args: {
81
+ config: CountyPatterns
82
+ },
83
+ parameters: {
84
+ docs: {
85
+ description: {
86
+ story:
87
+ 'County-map zoom baseline. Useful for checking that pan/zoom controls still behave correctly on county geographies.'
88
+ }
89
+ }
90
+ },
91
+ play: async ({ canvasElement }) => {
92
+ await verifyBasicZoomCycle(canvasElement)
93
+ }
94
+ }
95
+
96
+ export const World: Story = {
97
+ args: {
98
+ config: MultiCountry
99
+ },
100
+ parameters: {
101
+ docs: {
102
+ description: {
103
+ story:
104
+ 'World-map zoom baseline. Useful for checking that world-map zoom and reset controls still behave after single-state zoom changes.'
105
+ }
106
+ }
107
+ },
108
+ play: async ({ canvasElement }) => {
109
+ await verifyBasicZoomCycle(canvasElement)
110
+ }
111
+ }