@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
@@ -12,8 +12,10 @@ import MultiCountryHide from './_mock/multi-country-hide.json'
12
12
  import SingleStateWithFilters from './_mock/DEV-8942.json'
13
13
  import exampleCityState from './_mock/example-city-state.json'
14
14
  import USBubbleCities from './_mock/us-bubble-cities.json'
15
+ import worldBubbleReset from './_mock/world-bubble-reset.json'
15
16
  import { editConfigKeys } from '@cdc/core/helpers/configHelpers'
16
17
  import exampleLegendBins from './_mock/legend-bins.json'
18
+ import { performAndAssert, waitForPresence } from '@cdc/core/helpers/testing'
17
19
 
18
20
  // Fallback step function for test descriptions
19
21
  const step = async (description: string, fn: () => Promise<void> | void) => {
@@ -198,6 +200,52 @@ export const Bubble_Map: Story = {
198
200
  }
199
201
  }
200
202
 
203
+ export const World_Bubble_Reset_Restores_All_Bubbles: Story = {
204
+ args: {
205
+ config: worldBubbleReset,
206
+ isEditor: true
207
+ },
208
+ play: async ({ canvasElement }) => {
209
+ await assertVisualizationRendered(canvasElement)
210
+ await waitForPresence('circle.bubble.country--France', canvasElement)
211
+
212
+ const getBubbleState = () => ({
213
+ bubbleCount: canvasElement.querySelectorAll('circle.bubble[data-tooltip-id]').length
214
+ })
215
+
216
+ const dispatchBubblePointerClick = (bubble: Element) => {
217
+ const rect = bubble.getBoundingClientRect()
218
+ const clientX = rect.left + rect.width / 2
219
+ const clientY = rect.top + rect.height / 2
220
+
221
+ bubble.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, clientX, clientY }))
222
+ bubble.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, clientX, clientY }))
223
+ }
224
+
225
+ await performAndAssert(
226
+ 'World bubble click narrows the rendered bubble set',
227
+ getBubbleState,
228
+ async () => {
229
+ const franceBubble = canvasElement.querySelector('circle.bubble.country--France')
230
+ expect(franceBubble).toBeTruthy()
231
+ dispatchBubblePointerClick(franceBubble as Element)
232
+ },
233
+ (before, after) => after.bubbleCount > 0 && after.bubbleCount < before.bubbleCount
234
+ )
235
+
236
+ await performAndAssert(
237
+ 'Reset Zoom restores all world bubbles',
238
+ getBubbleState,
239
+ async () => {
240
+ const resetButton = canvasElement.querySelector('button[aria-label="Reset Zoom"]') as HTMLButtonElement | null
241
+ expect(resetButton).toBeTruthy()
242
+ resetButton?.click()
243
+ },
244
+ (_before, after) => after.bubbleCount === worldBubbleReset.data.length
245
+ )
246
+ }
247
+ }
248
+
201
249
  export const HHS_Region_Map: Story = {
202
250
  args: {
203
251
  configUrl: 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/examples/example-hhs-regions-data.json'
@@ -0,0 +1,65 @@
1
+ {
2
+ "version": "4.26.4",
3
+ "color": "pinkpurple",
4
+ "general": {
5
+ "title": "COVID-19 Case Rates by State",
6
+ "geoType": "us",
7
+ "type": "data",
8
+ "showTitle": true
9
+ },
10
+ "type": "map",
11
+ "dataMetadata": {
12
+ "altDescription": "Choropleth map of the United States showing COVID-19 case rates per 100,000 population. Rates are highest in the Southeast, with Alabama, Mississippi, and Louisiana exceeding 50 per 100,000.",
13
+ "lastUpdated": "2024-09-21",
14
+ "source": "CDC COVID Data Tracker"
15
+ },
16
+ "columns": {
17
+ "geo": {
18
+ "name": "FIPS Codes",
19
+ "label": "Location",
20
+ "tooltip": false,
21
+ "dataTable": true
22
+ },
23
+ "primary": {
24
+ "name": "Rate",
25
+ "label": "Rate per 100k",
26
+ "prefix": "",
27
+ "suffix": "",
28
+ "tooltip": true,
29
+ "dataTable": true
30
+ },
31
+ "navigate": { "name": "" },
32
+ "latitude": { "name": "" },
33
+ "longitude": { "name": "" }
34
+ },
35
+ "legend": {
36
+ "type": "equalnumber",
37
+ "numberOfItems": 3,
38
+ "position": "side",
39
+ "style": "circles",
40
+ "title": "Rate per 100k",
41
+ "description": "",
42
+ "descriptions": {},
43
+ "specialClasses": [],
44
+ "unified": false,
45
+ "singleColumn": false,
46
+ "singleRow": false,
47
+ "verticalSorted": false,
48
+ "showSpecialClassesLast": false,
49
+ "dynamicDescription": false,
50
+ "hideBorder": false
51
+ },
52
+ "filters": [],
53
+ "filterBehavior": "Filter Change",
54
+ "data": [
55
+ { "FIPS Codes": "01", "Rate": 55 },
56
+ { "FIPS Codes": "04", "Rate": 22 },
57
+ { "FIPS Codes": "06", "Rate": 31 },
58
+ { "FIPS Codes": "12", "Rate": 44 },
59
+ { "FIPS Codes": "13", "Rate": 48 },
60
+ { "FIPS Codes": "22", "Rate": 52 },
61
+ { "FIPS Codes": "28", "Rate": 58 },
62
+ { "FIPS Codes": "36", "Rate": 27 },
63
+ { "FIPS Codes": "48", "Rate": 38 }
64
+ ]
65
+ }
@@ -0,0 +1,138 @@
1
+ {
2
+ "annotations": [],
3
+ "general": {
4
+ "title": "World Bubble Reset Demo",
5
+ "subtext": "Click a bubble to zoom, then use Reset Zoom to restore all bubbles.",
6
+ "type": "bubble",
7
+ "geoType": "world",
8
+ "headerColor": "theme-blue",
9
+ "showSidebar": true,
10
+ "showTitle": true,
11
+ "showDownloadButton": true,
12
+ "expandDataTable": false,
13
+ "backgroundColor": "#ffffff",
14
+ "geoBorderColor": "darkGray",
15
+ "territoriesLabel": "Territories",
16
+ "language": "en",
17
+ "hasRegions": false,
18
+ "showDownloadMediaButton": false,
19
+ "fullBorder": false,
20
+ "palette": {
21
+ "isReversed": false
22
+ },
23
+ "allowMapZoom": true,
24
+ "hideGeoColumnInTooltip": false,
25
+ "hidePrimaryColumnInTooltip": false,
26
+ "geoLabelOverride": "",
27
+ "noDataFoundMessage": "No data found"
28
+ },
29
+ "type": "map",
30
+ "color": "bluegreen",
31
+ "columns": {
32
+ "geo": {
33
+ "name": "country",
34
+ "label": "Country",
35
+ "tooltip": true,
36
+ "dataTable": true
37
+ },
38
+ "primary": {
39
+ "dataTable": true,
40
+ "tooltip": true,
41
+ "prefix": "",
42
+ "suffix": "",
43
+ "name": "cases",
44
+ "label": "Cases"
45
+ },
46
+ "navigate": {
47
+ "name": ""
48
+ }
49
+ },
50
+ "legend": {
51
+ "descriptions": {},
52
+ "specialClasses": [],
53
+ "unified": false,
54
+ "singleColumn": true,
55
+ "dynamicDescription": false,
56
+ "type": "equalinterval",
57
+ "numberOfItems": 5,
58
+ "position": "side",
59
+ "title": "Cases",
60
+ "showTitle": true,
61
+ "reversedOrder": false,
62
+ "singleColumnLegend": false,
63
+ "hideBorder": false
64
+ },
65
+ "filters": [],
66
+ "table": {
67
+ "label": "Data Table",
68
+ "expanded": false,
69
+ "limitHeight": false,
70
+ "height": "",
71
+ "caption": "",
72
+ "showDownloadUrl": false,
73
+ "showDataTableLink": true,
74
+ "showFullGeoNameInCSV": false,
75
+ "forceDisplay": true,
76
+ "download": true,
77
+ "indexLabel": "",
78
+ "wrapColumns": false,
79
+ "showDownloadLinkBelow": true
80
+ },
81
+ "tooltips": {
82
+ "appearanceType": "hover",
83
+ "linkLabel": "Learn More",
84
+ "capitalizeLabels": true,
85
+ "opacity": 90
86
+ },
87
+ "runtime": {
88
+ "editorErrorMessage": []
89
+ },
90
+ "visual": {
91
+ "minBubbleSize": 10,
92
+ "maxBubbleSize": 28,
93
+ "extraBubbleBorder": false,
94
+ "cityStyle": "circle",
95
+ "geoCodeCircleSize": 12,
96
+ "showBubbleZeros": true,
97
+ "cityStyleLabel": "Bubble",
98
+ "additionalCityStyles": []
99
+ },
100
+ "mapPosition": {
101
+ "coordinates": [0, 30],
102
+ "zoom": 1
103
+ },
104
+ "map": {
105
+ "layers": [],
106
+ "patterns": []
107
+ },
108
+ "hexMap": {
109
+ "type": "",
110
+ "shapeGroups": []
111
+ },
112
+ "data": [
113
+ {
114
+ "country": "France",
115
+ "cases": 120
116
+ },
117
+ {
118
+ "country": "Brazil",
119
+ "cases": 340
120
+ },
121
+ {
122
+ "country": "Japan",
123
+ "cases": 280
124
+ },
125
+ {
126
+ "country": "Australia",
127
+ "cases": 200
128
+ },
129
+ {
130
+ "country": "South Africa",
131
+ "cases": 160
132
+ },
133
+ {
134
+ "country": "Canada",
135
+ "cases": 240
136
+ }
137
+ ]
138
+ }
@@ -0,0 +1,166 @@
1
+ {
2
+ "annotations": [],
3
+ "general": {
4
+ "title": "World Data Map Zoom With Filters",
5
+ "subtext": "Use the region filter, zoom into countries, and test reset behavior on a world data map.",
6
+ "type": "data",
7
+ "geoType": "world",
8
+ "headerColor": "theme-blue",
9
+ "showSidebar": true,
10
+ "showTitle": true,
11
+ "showDownloadButton": true,
12
+ "expandDataTable": false,
13
+ "backgroundColor": "#ffffff",
14
+ "geoBorderColor": "darkGray",
15
+ "territoriesLabel": "Territories",
16
+ "language": "en",
17
+ "hasRegions": false,
18
+ "showDownloadMediaButton": false,
19
+ "fullBorder": false,
20
+ "palette": {
21
+ "isReversed": false
22
+ },
23
+ "allowMapZoom": true,
24
+ "hideGeoColumnInTooltip": false,
25
+ "hidePrimaryColumnInTooltip": false,
26
+ "geoLabelOverride": "",
27
+ "noDataFoundMessage": "No data found"
28
+ },
29
+ "type": "map",
30
+ "color": "bluegreen",
31
+ "columns": {
32
+ "geo": {
33
+ "name": "Country",
34
+ "label": "Country",
35
+ "tooltip": true,
36
+ "dataTable": true
37
+ },
38
+ "primary": {
39
+ "name": "Value",
40
+ "label": "Value",
41
+ "prefix": "",
42
+ "suffix": "",
43
+ "tooltip": true,
44
+ "dataTable": true
45
+ },
46
+ "navigate": {
47
+ "name": ""
48
+ },
49
+ "additionalColumn1": {
50
+ "name": "Region",
51
+ "label": "Region",
52
+ "tooltip": true,
53
+ "dataTable": true,
54
+ "prefix": "",
55
+ "suffix": ""
56
+ }
57
+ },
58
+ "legend": {
59
+ "descriptions": {},
60
+ "specialClasses": [],
61
+ "unified": false,
62
+ "singleColumn": true,
63
+ "dynamicDescription": false,
64
+ "type": "equalinterval",
65
+ "numberOfItems": 5,
66
+ "position": "side",
67
+ "title": "Value",
68
+ "showTitle": true,
69
+ "reversedOrder": false,
70
+ "singleColumnLegend": false,
71
+ "hideBorder": false
72
+ },
73
+ "filters": [
74
+ {
75
+ "order": "asc",
76
+ "label": "Region",
77
+ "columnName": "Region",
78
+ "values": ["Europe", "Americas", "Asia-Pacific"],
79
+ "active": "Europe",
80
+ "filterStyle": "dropdown"
81
+ }
82
+ ],
83
+ "table": {
84
+ "label": "Data Table",
85
+ "expanded": false,
86
+ "limitHeight": false,
87
+ "height": "",
88
+ "caption": "",
89
+ "showDownloadUrl": false,
90
+ "showDataTableLink": true,
91
+ "showFullGeoNameInCSV": false,
92
+ "forceDisplay": true,
93
+ "download": true,
94
+ "indexLabel": "",
95
+ "wrapColumns": false,
96
+ "showDownloadLinkBelow": true
97
+ },
98
+ "tooltips": {
99
+ "appearanceType": "hover",
100
+ "linkLabel": "Learn More",
101
+ "capitalizeLabels": true,
102
+ "opacity": 90
103
+ },
104
+ "runtime": {
105
+ "editorErrorMessage": []
106
+ },
107
+ "visual": {
108
+ "minBubbleSize": 10,
109
+ "maxBubbleSize": 28,
110
+ "extraBubbleBorder": false,
111
+ "cityStyle": "circle",
112
+ "geoCodeCircleSize": 12,
113
+ "showBubbleZeros": true,
114
+ "cityStyleLabel": "Bubble",
115
+ "additionalCityStyles": []
116
+ },
117
+ "mapPosition": {
118
+ "coordinates": [0, 30],
119
+ "zoom": 1
120
+ },
121
+ "map": {
122
+ "layers": [],
123
+ "patterns": []
124
+ },
125
+ "hexMap": {
126
+ "type": "",
127
+ "shapeGroups": []
128
+ },
129
+ "data": [
130
+ {
131
+ "Country": "France",
132
+ "Value": 120,
133
+ "Region": "Europe"
134
+ },
135
+ {
136
+ "Country": "Germany",
137
+ "Value": 150,
138
+ "Region": "Europe"
139
+ },
140
+ {
141
+ "Country": "Italy",
142
+ "Value": 105,
143
+ "Region": "Europe"
144
+ },
145
+ {
146
+ "Country": "Brazil",
147
+ "Value": 210,
148
+ "Region": "Americas"
149
+ },
150
+ {
151
+ "Country": "Canada",
152
+ "Value": 175,
153
+ "Region": "Americas"
154
+ },
155
+ {
156
+ "Country": "Japan",
157
+ "Value": 190,
158
+ "Region": "Asia-Pacific"
159
+ },
160
+ {
161
+ "Country": "Australia",
162
+ "Value": 165,
163
+ "Region": "Asia-Pacific"
164
+ }
165
+ ]
166
+ }
@@ -64,11 +64,16 @@ const BubbleList: React.FC<BubbleListProps> = ({ customProjection }) => {
64
64
 
65
65
  // Zoom the map in...
66
66
  dispatch({ type: 'SET_POSITION', payload: { coordinates: reversedCoordinates, zoom: 3 } })
67
+ dispatch({ type: 'SET_FILTERED_COUNTRY_CODE', payload: _filteredCountryCode })
67
68
 
68
69
  // ...and show the data for the clicked country
69
70
  dispatch({ type: 'SET_RUNTIME_DATA', payload: _tempRuntimeData })
70
71
  }
71
72
 
73
+ const handleBubblePointerDown = (e: React.PointerEvent<SVGCircleElement> | React.MouseEvent<SVGCircleElement>) => {
74
+ e.preventDefault()
75
+ }
76
+
72
77
  const sortedRuntimeData: DataRow = Object.values(runtimeData).sort((a, b) =>
73
78
  a[primaryColumnName] < b[primaryColumnName] ? 1 : -1
74
79
  )
@@ -112,7 +117,9 @@ const BubbleList: React.FC<BubbleListProps> = ({ customProjection }) => {
112
117
  strokeWidth={1.25}
113
118
  fillOpacity={0.4}
114
119
  onMouseEnter={() => {}}
120
+ onMouseDown={handleBubblePointerDown}
115
121
  onPointerDown={e => {
122
+ handleBubblePointerDown(e)
116
123
  pointerX = e.clientX
117
124
  pointerY = e.clientY
118
125
  }}
@@ -148,7 +155,9 @@ const BubbleList: React.FC<BubbleListProps> = ({ customProjection }) => {
148
155
  stroke={'white'}
149
156
  strokeWidth={0.5}
150
157
  onMouseEnter={() => {}}
158
+ onMouseDown={handleBubblePointerDown}
151
159
  onPointerDown={e => {
160
+ handleBubblePointerDown(e)
152
161
  pointerX = e.clientX
153
162
  pointerY = e.clientY
154
163
  }}
@@ -226,7 +235,9 @@ const BubbleList: React.FC<BubbleListProps> = ({ customProjection }) => {
226
235
  strokeWidth={1.25}
227
236
  fillOpacity={0.4}
228
237
  onMouseEnter={() => {}}
238
+ onMouseDown={handleBubblePointerDown}
229
239
  onPointerDown={e => {
240
+ handleBubblePointerDown(e)
230
241
  pointerX = e.clientX
231
242
  pointerY = e.clientY
232
243
  }}
@@ -262,7 +273,9 @@ const BubbleList: React.FC<BubbleListProps> = ({ customProjection }) => {
262
273
  strokeWidth={0.5}
263
274
  fillOpacity={0.4}
264
275
  onMouseEnter={() => {}}
276
+ onMouseDown={handleBubblePointerDown}
265
277
  onPointerDown={e => {
278
+ handleBubblePointerDown(e)
266
279
  pointerX = e.clientX
267
280
  pointerY = e.clientY
268
281
  }}
@@ -1,6 +1,7 @@
1
1
  import React, { useContext, useEffect, useState, useMemo, useRef } from 'react'
2
2
  import { filterColorPalettes } from '@cdc/core/helpers/filterColorPalettes'
3
3
  import { cloneConfig } from '@cdc/core/helpers/cloneConfig'
4
+ import { resolveAltTextDescription } from '@cdc/core/helpers/resolveAltTextDescription'
4
5
 
5
6
  // Third Party
6
7
  import {
@@ -1831,6 +1832,105 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
1831
1832
  </Tooltip>
1832
1833
  }
1833
1834
  />
1835
+
1836
+ {/* Accessible Alt Text Description */}
1837
+ {(() => {
1838
+ const metadataKeys = Object.keys(config.dataMetadata || {})
1839
+ const hasMetadata = metadataKeys.length > 0
1840
+ const descType = config.altText?.type || ''
1841
+ const resolvedDescription = resolveAltTextDescription(config.altText, config.dataMetadata)
1842
+ return (
1843
+ <>
1844
+ <Select
1845
+ value={descType}
1846
+ fieldName='altTextType'
1847
+ label='Alt Text Description'
1848
+ options={[
1849
+ { value: '', label: 'None' },
1850
+ { value: 'static', label: 'Static (manual text)' },
1851
+ { value: 'metadata', label: 'Data File Metadata' }
1852
+ ]}
1853
+ updateField={(_section, _subsection, _fieldName, value) => {
1854
+ if (value === '') {
1855
+ updateField(null, null, 'altText', undefined)
1856
+ } else {
1857
+ updateField(null, null, 'altText', { type: value as 'static' | 'metadata' })
1858
+ }
1859
+ }}
1860
+ tooltip={
1861
+ <Tooltip style={{ textTransform: 'none' }}>
1862
+ <Tooltip.Target>
1863
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
1864
+ </Tooltip.Target>
1865
+ <Tooltip.Content>
1866
+ <p>
1867
+ Add a longer description for screen readers. The map title is always auto-generated.
1868
+ Use "Static" for manually written text, or "Data File Metadata" to pull it from a
1869
+ key in your data file.
1870
+ </p>
1871
+ </Tooltip.Content>
1872
+ </Tooltip>
1873
+ }
1874
+ />
1875
+ {descType === 'static' && (
1876
+ <TextField
1877
+ value={config.altText?.value || ''}
1878
+ fieldName='altTextValue'
1879
+ type='textarea'
1880
+ label='Description Text'
1881
+ placeholder='Longer interpretive description of map insights...'
1882
+ updateField={(_section, _subsection, _fieldName, value) => {
1883
+ updateField(null, null, 'altText', { ...config.altText, value })
1884
+ }}
1885
+ />
1886
+ )}
1887
+ {descType === 'metadata' && (
1888
+ <>
1889
+ {hasMetadata ? (
1890
+ <Select
1891
+ value={config.altText?.metadataKey || ''}
1892
+ fieldName='altTextMetadataKey'
1893
+ label='Description Metadata Field'
1894
+ options={[
1895
+ { value: '', label: 'Select Metadata Field...' },
1896
+ ...metadataKeys.map(key => ({
1897
+ value: key,
1898
+ label: `${key}: ${config.dataMetadata[key]}`
1899
+ }))
1900
+ ]}
1901
+ updateField={(_section, _subsection, _fieldName, value) => {
1902
+ updateField(null, null, 'altText', { ...config.altText, metadataKey: value })
1903
+ }}
1904
+ />
1905
+ ) : (
1906
+ <span className='subtext'>
1907
+ No metadata fields are available. Your data file must be a JSON object with a{' '}
1908
+ <code>data</code> array and sibling key-value pairs, for example:{' '}
1909
+ <code>{`{ "altDescription": "...", "data": [...] }`}</code>
1910
+ </span>
1911
+ )}
1912
+ </>
1913
+ )}
1914
+ {resolvedDescription && (
1915
+ <div
1916
+ style={{
1917
+ marginTop: '1em',
1918
+ padding: '0.75em',
1919
+ background: '#f5f5f5',
1920
+ borderRadius: '4px',
1921
+ fontSize: '0.8em',
1922
+ textTransform: 'none'
1923
+ }}
1924
+ >
1925
+ <strong style={{ display: 'block', marginBottom: '0.25em' }}>Preview:</strong>
1926
+ <p data-testid='alt-text-desc-preview' style={{ margin: 0, fontStyle: 'italic' }}>
1927
+ {resolvedDescription}
1928
+ </p>
1929
+ </div>
1930
+ )}
1931
+ </>
1932
+ )
1933
+ })()}
1834
1934
  </AccordionItemPanel>
1835
1935
  </AccordionItem>
1836
1936
  <AccordionItem>
@@ -3220,6 +3320,26 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
3220
3320
  label='Show collapse below table'
3221
3321
  updateField={updateField}
3222
3322
  />
3323
+ <CheckBox
3324
+ value={config.table.search ?? false}
3325
+ section='table'
3326
+ subsection={null}
3327
+ fieldName='search'
3328
+ label='Enable Search'
3329
+ updateField={updateField}
3330
+ />
3331
+ {config.table.search && (
3332
+ <div className='ms-4 mt-2' style={{ maxWidth: 'calc(100% - 1.5rem)' }}>
3333
+ <TextField
3334
+ value={config.table.searchPlaceholder || ''}
3335
+ section='table'
3336
+ fieldName='searchPlaceholder'
3337
+ label='Search Placeholder Text'
3338
+ placeholder='Filter...'
3339
+ updateField={updateField}
3340
+ />
3341
+ </div>
3342
+ )}
3223
3343
  <Select
3224
3344
  value={config.table.defaultSort?.column || ''}
3225
3345
  fieldName='column'
@@ -3755,6 +3875,20 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
3755
3875
  <span className='edit-label'>Allow Map Zooming</span>
3756
3876
  </label>
3757
3877
  )}
3878
+ {config.general.geoType === 'us' && (
3879
+ <label className='checkbox'>
3880
+ <input
3881
+ type='checkbox'
3882
+ checked={config.general.showClearSelectionButton !== false}
3883
+ onChange={event => {
3884
+ const _newConfig = cloneConfig(config)
3885
+ _newConfig.general.showClearSelectionButton = event.target.checked
3886
+ setConfig(_newConfig)
3887
+ }}
3888
+ />
3889
+ <span className='edit-label'>Show Clear Selection Button</span>
3890
+ </label>
3891
+ )}
3758
3892
  {config.general.type === 'bubble' && (
3759
3893
  <label className='checkbox'>
3760
3894
  <input
@@ -0,0 +1,21 @@
1
+ import React, { useContext } from 'react'
2
+ import ConfigContext from '../context'
3
+ import { MapContext } from '../types/MapContext'
4
+ import './filterControls.styles.css'
5
+
6
+ const FilterControls: React.FC = () => {
7
+ const { config, clearSharedFilter, hasActiveSharedFilter } = useContext<MapContext>(ConfigContext)
8
+
9
+ if (config.general.showClearSelectionButton === false) return null
10
+ if (!hasActiveSharedFilter || !clearSharedFilter) return null
11
+
12
+ return (
13
+ <div className='filter-controls' data-html2canvas-ignore='true'>
14
+ <button className='cove-button' onClick={() => clearSharedFilter(config.uid)} aria-label='Clear Selection'>
15
+ Clear Selection
16
+ </button>
17
+ </div>
18
+ )
19
+ }
20
+
21
+ export default FilterControls
@@ -1,4 +1,4 @@
1
- import React, { useContext, useMemo, useRef, useEffect, useCallback } from 'react'
1
+ import React, { useContext, useMemo, useRef, useCallback, useLayoutEffect } from 'react'
2
2
  import SmallMultipleTile from './SmallMultipleTile'
3
3
  import ConfigContext from '../../context'
4
4
  import { MapContext } from '../../types/MapContext'
@@ -71,7 +71,7 @@ const SmallMultiples: React.FC<SmallMultiplesProps> = () => {
71
71
  )
72
72
 
73
73
  // Align tile header heights per row
74
- useEffect(() => {
74
+ useLayoutEffect(() => {
75
75
  const headerEntries = Object.entries(headerRefs.current).filter(([_, ref]) => ref) as TileHeaderEntries
76
76
  if (headerEntries.length === 0) return
77
77