@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.
- package/CONFIG.md +70 -37
- package/LICENSE +201 -0
- package/README.md +6 -2
- package/dist/cdcmap.js +23502 -22964
- package/examples/default-county.json +3 -0
- package/examples/minimal-example.json +6 -2
- package/package.json +3 -3
- package/src/CdcMapComponent.tsx +13 -3
- package/src/_stories/CdcMap.AltText.stories.tsx +122 -0
- package/src/_stories/CdcMap.Editor.ColumnsSectionTests.stories.tsx +15 -16
- package/src/_stories/CdcMap.FocusVisibility.stories.tsx +87 -0
- package/src/_stories/CdcMap.HiddenMount.stories.tsx +69 -0
- package/src/_stories/CdcMap.ResetBehavior.stories.tsx +32 -0
- package/src/_stories/CdcMap.Zoom.stories.tsx +111 -0
- package/src/_stories/CdcMap.smoke.stories.tsx +48 -0
- package/src/_stories/_mock/alt_text_metadata.json +65 -0
- package/src/_stories/_mock/world-bubble-reset.json +138 -0
- package/src/_stories/_mock/world-data-zoom-filters.json +166 -0
- package/src/components/BubbleList.tsx +13 -0
- package/src/components/EditorPanel/components/EditorPanel.tsx +134 -0
- package/src/components/FilterControls.tsx +21 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +2 -2
- package/src/components/UsaMap/components/UsaMap.County.tsx +39 -9
- package/src/components/UsaMap/components/UsaMap.Region.tsx +5 -2
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +33 -10
- package/src/components/UsaMap/components/UsaMap.State.tsx +9 -2
- package/src/components/WorldMap/WorldMap.tsx +37 -4
- package/src/components/ZoomableGroup.tsx +23 -3
- package/src/components/filterControls.styles.css +6 -0
- package/src/data/initial-state.js +2 -0
- package/src/helpers/countyTerritories.ts +1 -1
- package/src/helpers/generateRuntimeFilters.ts +2 -1
- package/src/helpers/handleMapAriaLabels.ts +45 -30
- package/src/helpers/shouldAutoResetSingleStateZoom.ts +22 -0
- package/src/helpers/tests/handleMapAriaLabels.test.ts +71 -0
- package/src/helpers/tests/shouldAutoResetSingleStateZoom.test.ts +71 -0
- package/src/hooks/useGeoClickHandler.ts +13 -1
- package/src/hooks/useStateZoom.tsx +39 -20
- package/src/hooks/useTooltip.test.tsx +2 -16
- package/src/index.jsx +5 -2
- package/src/scss/main.scss +6 -21
- package/src/scss/map.scss +20 -0
- package/src/types/MapConfig.ts +5 -0
- package/src/types/MapContext.ts +3 -0
|
@@ -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.
|
|
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.
|
|
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": "
|
|
45
|
+
"gitHead": "61c025165d96b45a6002c34582c5a622a9d865a9",
|
|
46
46
|
"main": "dist/cdcmap",
|
|
47
47
|
"moduleName": "CdcMap",
|
|
48
48
|
"peerDependencies": {
|
package/src/CdcMapComponent.tsx
CHANGED
|
@@ -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={{
|
|
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
|
-
|
|
582
|
+
hasVisibleVizFilters(filterConfig.filters) ? (
|
|
574
583
|
<Filters
|
|
575
|
-
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
|
-
//
|
|
437
|
-
const
|
|
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
|
-
|
|
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
|
-
//
|
|
455
|
-
return before.
|
|
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
|
-
//
|
|
467
|
-
return !before.
|
|
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
|
-
|
|
486
|
-
|
|
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
|
-
|
|
492
|
-
|
|
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
|
-
//
|
|
505
|
-
return before.
|
|
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
|
-
//
|
|
517
|
-
return
|
|
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
|
+
}
|