@cdc/core 4.24.5 → 4.24.9

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 (109) hide show
  1. package/assets/icon-gear-multi.svg +23 -0
  2. package/components/AdvancedEditor/AdvancedEditor.tsx +93 -0
  3. package/components/AdvancedEditor/advanced-editor-styles.css +3 -0
  4. package/components/AdvancedEditor/index.ts +1 -0
  5. package/components/Alert/components/Alert.styles.css +15 -0
  6. package/components/Alert/components/Alert.tsx +39 -0
  7. package/components/Alert/index.tsx +3 -0
  8. package/components/DataTable/DataTable.tsx +127 -32
  9. package/components/DataTable/DataTableStandAlone.tsx +4 -25
  10. package/components/DataTable/components/DataTableEditorPanel.tsx +4 -4
  11. package/components/DataTable/components/ExpandCollapse.tsx +1 -1
  12. package/components/DataTable/helpers/chartCellMatrix.tsx +6 -12
  13. package/components/DataTable/helpers/getChartCellValue.ts +9 -5
  14. package/components/DataTable/helpers/getDataSeriesColumns.ts +10 -7
  15. package/components/DataTable/helpers/getRowType.ts +6 -0
  16. package/components/DataTable/helpers/mapCellMatrix.tsx +3 -3
  17. package/components/DataTable/types/TableConfig.ts +2 -1
  18. package/components/EditorPanel/ColumnsEditor.tsx +3 -30
  19. package/components/EditorPanel/DataTableEditor.tsx +66 -22
  20. package/components/EditorPanel/FieldSetWrapper.tsx +51 -0
  21. package/components/EditorPanel/FootnotesEditor.tsx +77 -0
  22. package/components/EditorPanel/Inputs.tsx +13 -4
  23. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +268 -0
  24. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +306 -0
  25. package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +40 -0
  26. package/components/EditorPanel/VizFilterEditor/index.ts +1 -0
  27. package/components/EditorWrapper/EditorWrapper.tsx +3 -4
  28. package/components/EditorWrapper/index.ts +1 -0
  29. package/components/Filters.tsx +520 -0
  30. package/components/Footnotes/Footnotes.tsx +25 -0
  31. package/components/Footnotes/FootnotesStandAlone.tsx +45 -0
  32. package/components/Footnotes/footnotes.css +5 -0
  33. package/components/Footnotes/index.ts +1 -0
  34. package/components/Layout/components/Responsive.tsx +14 -4
  35. package/components/Layout/components/Sidebar/components/Sidebar.tsx +14 -5
  36. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +23 -20
  37. package/components/Layout/components/Visualization/index.tsx +19 -6
  38. package/components/Layout/components/Visualization/visualizations.scss +32 -26
  39. package/components/Layout/styles/editor.scss +0 -8
  40. package/components/Legend/Legend.Gradient.tsx +133 -0
  41. package/components/LegendShape.tsx +28 -0
  42. package/components/MultiSelect/MultiSelect.tsx +41 -11
  43. package/components/MultiSelect/multiselect.styles.css +0 -3
  44. package/components/NestedDropdown/NestedDropdown.tsx +47 -52
  45. package/components/NestedDropdown/nesteddropdown.styles.css +19 -25
  46. package/components/Table/Table.tsx +8 -5
  47. package/components/Table/components/Cell.tsx +2 -2
  48. package/components/Table/components/Row.tsx +25 -7
  49. package/components/_stories/Footnotes.stories.tsx +17 -0
  50. package/components/_stories/Layout.Debug.stories.tsx +91 -0
  51. package/components/_stories/_mocks/bar-chart-suppressed.json +474 -0
  52. package/components/_stories/styles.scss +14 -1
  53. package/components/createBarElement.jsx +4 -4
  54. package/components/inputs/InputSelect.tsx +17 -6
  55. package/components/ui/Icon.tsx +22 -16
  56. package/components/ui/Title/Title.scss +0 -8
  57. package/helpers/DataTransform.ts +2 -2
  58. package/helpers/addValuesToFilters.ts +135 -0
  59. package/helpers/cove/accessibility.ts +17 -4
  60. package/helpers/cove/fontSettings.ts +2 -0
  61. package/helpers/coveUpdateWorker.ts +30 -9
  62. package/helpers/filterVizData.ts +49 -0
  63. package/helpers/formatConfigBeforeSave.ts +95 -0
  64. package/helpers/gatherQueryParams.ts +14 -7
  65. package/helpers/getGradientLegendWidth.ts +15 -0
  66. package/helpers/getTextWidth.ts +18 -0
  67. package/helpers/lineChartHelpers.js +2 -1
  68. package/helpers/pivotData.ts +18 -0
  69. package/helpers/queryStringUtils.ts +29 -0
  70. package/helpers/scaling.ts +7 -0
  71. package/helpers/tests/addValuesToFilters.test.ts +55 -0
  72. package/helpers/tests/filterVizData.test.ts +31 -0
  73. package/helpers/tests/invertValue.test.ts +35 -0
  74. package/helpers/tests/updateFieldFactory.test.ts +1 -0
  75. package/helpers/updateFieldFactory.ts +1 -1
  76. package/helpers/updatePaletteNames.ts +19 -0
  77. package/helpers/{useDataVizClasses.js → useDataVizClasses.ts} +3 -2
  78. package/helpers/ver/4.24.5.ts +3 -3
  79. package/helpers/ver/4.24.7.ts +123 -0
  80. package/helpers/ver/4.24.9.ts +63 -0
  81. package/helpers/ver/tests/4.24.9.test.ts +22 -0
  82. package/helpers/ver/versionNeedsUpdate.ts +9 -0
  83. package/package.json +6 -4
  84. package/styles/_button-section.scss +7 -2
  85. package/styles/_data-table.scss +0 -1
  86. package/styles/_global.scss +6 -2
  87. package/styles/base.scss +4 -0
  88. package/styles/filters.scss +4 -0
  89. package/styles/v2/themes/_color-definitions.scss +1 -0
  90. package/types/Annotation.ts +46 -0
  91. package/types/Axis.ts +3 -2
  92. package/types/ConfigureData.ts +1 -1
  93. package/types/Dimensions.ts +1 -0
  94. package/types/Footnotes.ts +17 -0
  95. package/types/General.ts +5 -0
  96. package/types/Runtime.ts +2 -7
  97. package/types/Table.ts +6 -0
  98. package/types/Visualization.ts +31 -9
  99. package/types/VizFilter.ts +39 -7
  100. package/LICENSE +0 -201
  101. package/components/AdvancedEditor.jsx +0 -74
  102. package/components/EditorPanel/VizFilterEditor.tsx +0 -234
  103. package/components/Filters.jsx +0 -461
  104. package/components/LegendCircle.jsx +0 -17
  105. package/helpers/queryStringUtils.js +0 -26
  106. package/helpers/updatePaletteNames.js +0 -16
  107. package/types/BaseVisualizationType.ts +0 -1
  108. /package/components/{Waiting.jsx → Waiting.tsx} +0 -0
  109. /package/helpers/ver/{4.23.4.ts → 4.24.4.ts} +0 -0
@@ -0,0 +1,520 @@
1
+ import { useState, useEffect, useMemo } from 'react'
2
+ import { useId } from 'react'
3
+
4
+ // CDC
5
+ import Button from './elements/Button'
6
+ import { getQueryParams, updateQueryString } from '../helpers/queryStringUtils'
7
+ import MultiSelect from './MultiSelect'
8
+ import { Visualization } from '../types/Visualization'
9
+ import { MultiSelectFilter, OrderBy, VizFilter } from '../types/VizFilter'
10
+ import { filterVizData } from '../helpers/filterVizData'
11
+ import { addValuesToFilters } from '../helpers/addValuesToFilters'
12
+ import { DimensionsType } from '../types/Dimensions'
13
+ import NestedDropdown from './NestedDropdown'
14
+ import _ from 'lodash'
15
+
16
+ export const filterStyleOptions = ['dropdown', 'nested-dropdown', 'pill', 'tab', 'tab bar', 'multi-select']
17
+
18
+ export const filterOrderOptions: { label: string; value: OrderBy }[] = [
19
+ {
20
+ label: 'Ascending Alphanumeric',
21
+ value: 'asc'
22
+ },
23
+ {
24
+ label: 'Descending Alphanumeric',
25
+ value: 'desc'
26
+ },
27
+ {
28
+ label: 'Custom',
29
+ value: 'cust'
30
+ }
31
+ ]
32
+
33
+ export const handleSorting = singleFilter => {
34
+ const singleFilterValues = _.cloneDeep(singleFilter.values)
35
+ if (singleFilter.order === 'cust' && singleFilter.filterStyle !== 'nested-dropdown') {
36
+ singleFilter.values = singleFilter.orderedValues?.length ? singleFilter.orderedValues : singleFilterValues
37
+ return singleFilter
38
+ }
39
+
40
+ const sort = (a, b) => {
41
+ const asc = singleFilter.order !== 'desc'
42
+ return (asc ? a : b).toString().localeCompare((asc ? b : a).toString(), 'en', { numeric: true })
43
+ }
44
+
45
+ singleFilter.values = singleFilterValues.sort(sort)
46
+
47
+ return singleFilter
48
+ }
49
+
50
+ const hasStandardFilterBehavior = ['chart', 'table']
51
+
52
+ export const useFilters = props => {
53
+ const [showApplyButton, setShowApplyButton] = useState(false)
54
+
55
+ // Desconstructing: notice, adding more descriptive visualizationConfig name over config
56
+ // visualizationConfig feels more robust for all vis types so that its not confused with config/state/etc.
57
+ const { config: visualizationConfig, setConfig, filteredData, setFilteredData, excludedData, getUniqueValues } = props
58
+ const { type, data } = visualizationConfig
59
+
60
+ /**
61
+ * Re-orders a filter based on two indices and updates the runtime filters array and filters state
62
+ * @param {number} idx1 - The index of the original position of the filter value.
63
+ * @param {number} idx2 - The index of the new position for the filter value.
64
+ * @param {number} filterIndex - The index of the filter item within the array of filter items.
65
+ * @param {object} filter - The filter item itself, which contains an array of filter values.
66
+ * @return {void} None. This function only updates the state of the component.
67
+ *
68
+ * @modifies {object} - The filter object passed in as a parameter
69
+ * @modifies {array} - The filteredData state if visualizationConfig.type equals 'map'
70
+ * @modifies {object} - The visualizationConfig state
71
+ */
72
+ const handleFilterOrder = (idx1, idx2, filterIndex, filter) => {
73
+ // Create a shallow copy of the filter values array & update position of the values
74
+ const updatedValues = [...filter.values]
75
+ const [movedItem] = updatedValues.splice(idx1, 1)
76
+ updatedValues.splice(idx2, 0, movedItem)
77
+
78
+ const filtersCopy = hasStandardFilterBehavior.includes(visualizationConfig.type)
79
+ ? [...visualizationConfig.filters]
80
+ : [...filteredData]
81
+ const filterItem = { ...filtersCopy[filterIndex] }
82
+
83
+ // Overwrite filterItem.values since thats what we map through in the editor panel
84
+ filterItem.values = updatedValues
85
+ filterItem.orderedValues = updatedValues
86
+ filterItem.active = updatedValues[0]
87
+ filterItem.order = 'cust'
88
+
89
+ // Update the filters
90
+ filtersCopy[filterIndex] = filterItem
91
+
92
+ if (visualizationConfig.type === 'map') {
93
+ setFilteredData(filtersCopy)
94
+ }
95
+
96
+ setConfig({ ...visualizationConfig, filters: filtersCopy })
97
+ }
98
+
99
+ const changeFilterActive = (index, value) => {
100
+ let newFilters = visualizationConfig.type === 'map' ? [...filteredData] : [...visualizationConfig.filters]
101
+
102
+ if (visualizationConfig.filterBehavior === 'Apply Button') {
103
+ newFilters[index].queuedActive = value
104
+ setShowApplyButton(true)
105
+ } else {
106
+ const newFilter = newFilters[index]
107
+ if (newFilter.filterStyle !== 'nested-dropdown') {
108
+ newFilter.active = value
109
+ } else {
110
+ newFilter.active = value[0]
111
+ newFilter.subGrouping.active = value[1]
112
+ }
113
+
114
+ const queryParams = getQueryParams()
115
+ if (newFilter.setByQueryParameter && queryParams[newFilter.setByQueryParameter] !== newFilter.active) {
116
+ queryParams[newFilter.setByQueryParameter] = newFilter.active
117
+ updateQueryString(queryParams)
118
+ }
119
+ }
120
+
121
+ if (!visualizationConfig.dynamicSeries) {
122
+ newFilters = addValuesToFilters(newFilters, excludedData)
123
+ setConfig({
124
+ ...visualizationConfig,
125
+ filters: newFilters
126
+ })
127
+ }
128
+
129
+ // Used for setting active filter, fromHash breaks the filteredData functionality.
130
+ if (visualizationConfig.type === 'map' && visualizationConfig.filterBehavior === 'Filter Change') {
131
+ setFilteredData(newFilters)
132
+ }
133
+
134
+ // If we're on a chart and not using the apply button
135
+ if (
136
+ hasStandardFilterBehavior.includes(visualizationConfig.type) &&
137
+ visualizationConfig.filterBehavior === 'Filter Change'
138
+ ) {
139
+ const newFilteredData = filterVizData(newFilters, excludedData)
140
+ setFilteredData(newFilteredData)
141
+
142
+ if (visualizationConfig.dynamicSeries) {
143
+ const runtime = visualizationConfig.runtime || {}
144
+ runtime.series = []
145
+ runtime.seriesLabels = {}
146
+ runtime.seriesLabelsAll = []
147
+
148
+ if (newFilteredData && newFilteredData.length && newFilteredData.length > 0) {
149
+ Object.keys(newFilteredData[0]).forEach(seriesKey => {
150
+ if (
151
+ seriesKey !== visualizationConfig.xAxis.dataKey &&
152
+ newFilteredData[0][seriesKey] &&
153
+ (!visualizationConfig.filters ||
154
+ visualizationConfig.filters?.filter(filter => filter.columnName === seriesKey).length === 0) &&
155
+ (!visualizationConfig.columns || Object.keys(visualizationConfig.columns).indexOf(seriesKey) === -1)
156
+ ) {
157
+ runtime.series.push({
158
+ dataKey: seriesKey,
159
+ type: visualizationConfig.dynamicSeriesType,
160
+ lineType: visualizationConfig.dynamicSeriesLineType,
161
+ tooltip: true
162
+ })
163
+ }
164
+ })
165
+ }
166
+
167
+ runtime.seriesKeys = runtime.series
168
+ ? runtime.series.map(series => {
169
+ runtime.seriesLabels[series.dataKey] = series.name || series.label || series.dataKey
170
+ runtime.seriesLabelsAll.push(series.name || series.dataKey)
171
+ return series.dataKey
172
+ })
173
+ : []
174
+
175
+ setConfig({
176
+ ...visualizationConfig,
177
+ filters: newFilters,
178
+ runtime
179
+ })
180
+ }
181
+ }
182
+ }
183
+
184
+ const handleApplyButton = newFilters => {
185
+ let needsQueryUpdate = false
186
+ const queryParams = getQueryParams()
187
+ newFilters.forEach(newFilter => {
188
+ if (newFilter.queuedActive) {
189
+ newFilter.active = newFilter.queuedActive
190
+ delete newFilter.queuedActive
191
+ if (newFilter.setByQueryParameter && queryParams[newFilter.setByQueryParameter] !== newFilter.active) {
192
+ queryParams[newFilter.setByQueryParameter] = newFilter.active
193
+ needsQueryUpdate = true
194
+ }
195
+ }
196
+ })
197
+ if (needsQueryUpdate) {
198
+ updateQueryString(queryParams)
199
+ }
200
+
201
+ setConfig({ ...visualizationConfig, filters: newFilters })
202
+
203
+ if (type === 'map') {
204
+ setFilteredData(newFilters, excludedData)
205
+ }
206
+
207
+ if (hasStandardFilterBehavior.includes(visualizationConfig.type)) {
208
+ setFilteredData(filterVizData(newFilters, excludedData))
209
+ }
210
+
211
+ setShowApplyButton(false)
212
+ }
213
+
214
+ const handleReset = e => {
215
+ let newFilters = [...visualizationConfig.filters]
216
+ e.preventDefault()
217
+
218
+ // reset to first item in values array.
219
+ let needsQueryUpdate = false
220
+ const queryParams = getQueryParams()
221
+ newFilters.forEach((filter, i) => {
222
+ if (!filter.values || filter.values.length === 0) {
223
+ filter.values = getUniqueValues(data, filter.columnName)
224
+ }
225
+ newFilters[i].active = handleSorting(filter).values[0]
226
+
227
+ if (filter.setByQueryParameter && queryParams[filter.setByQueryParameter] !== filter.active) {
228
+ queryParams[filter.setByQueryParameter] = filter.active
229
+ needsQueryUpdate = true
230
+ }
231
+ })
232
+
233
+ if (needsQueryUpdate) {
234
+ updateQueryString(queryParams)
235
+ }
236
+
237
+ setConfig({ ...visualizationConfig, filters: newFilters })
238
+
239
+ if (type === 'map') {
240
+ setFilteredData(newFilters, excludedData)
241
+ } else {
242
+ setFilteredData(filterVizData(newFilters, excludedData))
243
+ }
244
+ }
245
+
246
+ const filterConstants = {
247
+ buttonText: 'Apply Filters',
248
+ resetText: 'Reset All',
249
+ introText: `Make a selection from the filters to change the visualization information.`,
250
+ applyText: 'Select the apply button to update the visualization information.'
251
+ }
252
+
253
+ // prettier-ignore
254
+ return {
255
+ handleApplyButton,
256
+ changeFilterActive,
257
+ showApplyButton,
258
+ handleReset,
259
+ filterConstants,
260
+ filterStyleOptions,
261
+ filterOrderOptions,
262
+ handleFilterOrder,
263
+ handleSorting
264
+ }
265
+ }
266
+
267
+ type FilterProps = {
268
+ filteredData: Object[]
269
+ dimensions: DimensionsType
270
+ config: Visualization
271
+ // function for updating the runtime filters
272
+ setFilteredData: Function
273
+ // updating function for setting fitlerBehavior
274
+ setConfig: Function
275
+ // exclusions
276
+ exclusions: any[]
277
+ }
278
+
279
+ const Filters = (props: FilterProps) => {
280
+ const { config: visualizationConfig, filteredData, dimensions } = props
281
+ const { filters, type, general, theme, filterBehavior } = visualizationConfig
282
+ const [mobileFilterStyle, setMobileFilterStyle] = useState(false)
283
+ const [selectedFilter, setSelectedFilter] = useState<EventTarget>(null)
284
+ const id = useId()
285
+
286
+ // useFilters hook provides data and logic for handling various filter functions
287
+ // prettier-ignore
288
+ const {
289
+ handleApplyButton,
290
+ changeFilterActive,
291
+ showApplyButton,
292
+ handleReset,
293
+ filterConstants,
294
+ handleSorting
295
+ } = useFilters(props)
296
+
297
+ useEffect(() => {
298
+ if (!dimensions) return
299
+ if (Number(dimensions[0]) < 768 && filters?.length > 0) {
300
+ setMobileFilterStyle(true)
301
+ } else {
302
+ setMobileFilterStyle(false)
303
+ }
304
+ }, [dimensions])
305
+
306
+ useEffect(() => {
307
+ if (selectedFilter) {
308
+ const el = document.getElementById(selectedFilter.id)
309
+ if (el) el.focus()
310
+ }
311
+ }, [changeFilterActive, selectedFilter])
312
+
313
+ const TabBar = props => {
314
+ const { filter: singleFilter, index: outerIndex } = props
315
+ return (
316
+ <section className='single-filters__tab-bar'>
317
+ {singleFilter.values.map((filter, index) => {
318
+ const buttonClassList = ['button__tab-bar', singleFilter.active === filter ? 'button__tab-bar--active' : '']
319
+ return (
320
+ <button
321
+ id={`${filter}-${outerIndex}-${index}-${id}`}
322
+ className={buttonClassList.join(' ')}
323
+ key={filter}
324
+ onClick={e => {
325
+ changeFilterActive(outerIndex, filter)
326
+ setSelectedFilter(e.target)
327
+ }}
328
+ onKeyDown={e => {
329
+ if (e.keyCode === 13) {
330
+ changeFilterActive(outerIndex, filter)
331
+ setSelectedFilter(e.target)
332
+ }
333
+ }}
334
+ >
335
+ {filter}
336
+ </button>
337
+ )
338
+ })}
339
+ </section>
340
+ )
341
+ }
342
+
343
+ const Dropdown = props => {
344
+ const { index: outerIndex, label, active, filters } = props
345
+ return (
346
+ <select
347
+ id={`filter-${outerIndex}`}
348
+ name={label}
349
+ aria-label={`Filter by ${label}`}
350
+ className='filter-select'
351
+ data-index='0'
352
+ value={active}
353
+ onChange={e => {
354
+ changeFilterActive(outerIndex, e.target.value)
355
+ }}
356
+ >
357
+ {filters}
358
+ </select>
359
+ )
360
+ }
361
+
362
+ const vizFiltersWithValues = useMemo(() => {
363
+ // Here charts is using config.filters where maps is using a runtime value
364
+ let vizfilters = type === 'map' ? filteredData : filters
365
+ if (!vizfilters) return []
366
+ if (vizfilters.fromHash) delete vizfilters.fromHash // support for Maps config
367
+ return addValuesToFilters(vizfilters as VizFilter[], visualizationConfig.data)
368
+ }, [filters, filteredData])
369
+
370
+ // Resolve Filter Styles
371
+ const Style = () => {
372
+ return vizFiltersWithValues.map((singleFilter: VizFilter, outerIndex) => {
373
+ if (singleFilter.showDropdown === false) return
374
+
375
+ const DropdownOptions = []
376
+ const Pills = []
377
+ const Tabs = []
378
+
379
+ const { active, queuedActive, label, filterStyle } = singleFilter as VizFilter
380
+
381
+ handleSorting(singleFilter)
382
+
383
+ singleFilter.values?.forEach((filterOption, index) => {
384
+ const pillClassList = ['pill', active === filterOption ? 'pill--active' : null, theme && theme]
385
+ const tabClassList = ['tab', active === filterOption && 'tab--active', theme && theme]
386
+
387
+ Pills.push(
388
+ <div className='pill__wrapper' key={`pill-${index}`}>
389
+ <button
390
+ id={`${filterOption}-${outerIndex}-${index}-${id}`}
391
+ className={pillClassList.join(' ')}
392
+ onKeyDown={e => {
393
+ if (e.keyCode === 13) {
394
+ changeFilterActive(outerIndex, filterOption)
395
+ setSelectedFilter(e.target)
396
+ }
397
+ }}
398
+ onClick={e => {
399
+ changeFilterActive(outerIndex, filterOption)
400
+ setSelectedFilter(e.target)
401
+ }}
402
+ name={label}
403
+ >
404
+ {filterOption}
405
+ </button>
406
+ </div>
407
+ )
408
+
409
+ DropdownOptions.push(
410
+ <option key={index} value={filterOption} aria-label={filterOption}>
411
+ {singleFilter.labels && singleFilter.labels[filterOption]
412
+ ? singleFilter.labels[filterOption]
413
+ : filterOption}
414
+ </option>
415
+ )
416
+
417
+ Tabs.push(
418
+ <button
419
+ id={`${filterOption}-${outerIndex}-${index}-${id}`}
420
+ className={tabClassList.join(' ')}
421
+ onClick={e => {
422
+ changeFilterActive(outerIndex, filterOption)
423
+ setSelectedFilter(e.target)
424
+ }}
425
+ onKeyDown={e => {
426
+ if (e.keyCode === 13) {
427
+ changeFilterActive(outerIndex, filterOption)
428
+ setSelectedFilter(e.target)
429
+ }
430
+ }}
431
+ >
432
+ {filterOption}
433
+ </button>
434
+ )
435
+ })
436
+
437
+ const classList = [
438
+ 'single-filters',
439
+ mobileFilterStyle ? 'single-filters--dropdown' : `single-filters--${filterStyle}`
440
+ ]
441
+ const mobileExempt = ['nested-dropdown', 'multi-select'].includes(filterStyle)
442
+ const showDefaultDropdown = (filterStyle === 'dropdown' || mobileFilterStyle) && !mobileExempt
443
+ return (
444
+ <div className={classList.join(' ')} key={outerIndex}>
445
+ <>
446
+ {label && <label htmlFor={`filter-${outerIndex}`}>{label}</label>}
447
+ {filterStyle === 'tab' && !mobileFilterStyle && Tabs}
448
+ {filterStyle === 'pill' && !mobileFilterStyle && Pills}
449
+ {filterStyle === 'tab bar' && !mobileFilterStyle && <TabBar filter={singleFilter} index={outerIndex} />}
450
+ {filterStyle === 'multi-select' && (
451
+ <MultiSelect
452
+ options={singleFilter.values.map(v => ({ value: v, label: v }))}
453
+ fieldName={outerIndex}
454
+ updateField={(_section, _subSection, fieldName, value) => changeFilterActive(fieldName, value)}
455
+ selected={singleFilter.active as string[]}
456
+ limit={(singleFilter as MultiSelectFilter).selectLimit || 5}
457
+ />
458
+ )}
459
+ {filterStyle === 'nested-dropdown' && (
460
+ <NestedDropdown
461
+ currentFilter={singleFilter}
462
+ listLabel={label}
463
+ handleSelectedItems={value => changeFilterActive(outerIndex, value)}
464
+ />
465
+ )}
466
+ {showDefaultDropdown && (
467
+ <Dropdown
468
+ filter={singleFilter}
469
+ index={outerIndex}
470
+ label={label}
471
+ active={queuedActive || active}
472
+ filters={DropdownOptions}
473
+ />
474
+ )}
475
+ </>
476
+ </div>
477
+ )
478
+ })
479
+ }
480
+
481
+ if (visualizationConfig?.filters?.length === 0) return
482
+ const filterSectionClassList = [
483
+ `filters-section legend_${visualizationConfig?.legend?.hide ? 'hidden' : 'visible'}_${
484
+ visualizationConfig?.legend?.position || ''
485
+ }`,
486
+ type === 'map' ? general.headerColor : visualizationConfig?.visualizationType === 'Spark Line' ? null : theme
487
+ ]
488
+ return (
489
+ <section className={filterSectionClassList.join(' ')}>
490
+ <p className='filters-section__intro-text'>
491
+ {filters?.some(f => f.active && f.showDropdown) ? filterConstants.introText : ''}{' '}
492
+ {visualizationConfig.filterBehavior === 'Apply Button' && filterConstants.applyText}
493
+ </p>
494
+ <div className='filters-section__wrapper'>
495
+ {' '}
496
+ <>
497
+ <Style />
498
+ {filterBehavior === 'Apply Button' ? (
499
+ <div className='filters-section__buttons'>
500
+ <Button
501
+ onClick={() => handleApplyButton(filters)}
502
+ disabled={!showApplyButton}
503
+ className={[general?.headerColor ? general.headerColor : theme, 'apply'].join(' ')}
504
+ >
505
+ {filterConstants.buttonText}
506
+ </Button>
507
+ <a href='#!' role='button' onClick={handleReset}>
508
+ {filterConstants.resetText}
509
+ </a>
510
+ </div>
511
+ ) : (
512
+ <></>
513
+ )}
514
+ </>
515
+ </div>
516
+ </section>
517
+ )
518
+ }
519
+
520
+ export default Filters
@@ -0,0 +1,25 @@
1
+ import { Footnote } from '../../types/Footnotes'
2
+ import './footnotes.css'
3
+
4
+ type FootnotesProps = {
5
+ footnotes: Footnote[]
6
+ }
7
+
8
+ const Footnotes: React.FC<FootnotesProps> = ({ footnotes }) => {
9
+ return (
10
+ <footer className='col-12 m-3 mt-1 mb-0'>
11
+ <ul className='cove-footnotes'>
12
+ {footnotes.map((note, i) => {
13
+ return (
14
+ <li key={note.symbol + i} className='mb-1'>
15
+ {note.symbol && <span className='mr-1'>{note.symbol}</span>}
16
+ {note.text}
17
+ </li>
18
+ )
19
+ })}
20
+ </ul>
21
+ </footer>
22
+ )
23
+ }
24
+
25
+ export default Footnotes
@@ -0,0 +1,45 @@
1
+ import EditorWrapper from '../EditorWrapper'
2
+ import Footnotes from './Footnotes'
3
+ import FootnotesEditor from '../EditorPanel/FootnotesEditor'
4
+ import { ViewPort } from '../../types/ViewPort'
5
+ import FootnotesConfig, { Footnote } from '../../types/Footnotes'
6
+ import _ from 'lodash'
7
+ import { useMemo } from 'react'
8
+ import { updateFieldFactory } from '../../helpers/updateFieldFactory'
9
+
10
+ type StandAloneProps = {
11
+ isEditor?: boolean
12
+ visualizationKey: string
13
+ config: FootnotesConfig
14
+ updateConfig?: (config: FootnotesConfig) => void
15
+ viewport?: ViewPort
16
+ }
17
+
18
+ const FootnotesStandAlone: React.FC<StandAloneProps> = ({ visualizationKey, config, viewport, isEditor, updateConfig }) => {
19
+ const updateField = updateFieldFactory<Footnote[]>(config, updateConfig)
20
+ if (isEditor)
21
+ return (
22
+ <EditorWrapper component={FootnotesStandAlone} visualizationKey={visualizationKey} visualizationConfig={config} updateConfig={updateConfig} type={'Footnotes'} viewport={viewport}>
23
+ <FootnotesEditor key={visualizationKey} config={config} updateField={updateField} />
24
+ </EditorWrapper>
25
+ )
26
+
27
+ // get the api footnotes from the config
28
+ const apiFootnotes = useMemo(() => {
29
+ if (config.dataKey && config.dynamicFootnotes) {
30
+ const { symbolColumn, textColumn, orderColumn } = config.dynamicFootnotes
31
+ const configData = config.formattedData || config.data
32
+ const _data = configData.map(row => _.pick(row, [symbolColumn, textColumn, orderColumn]))
33
+ _data.sort((a, b) => a[orderColumn] - b[orderColumn])
34
+ return _data.map(row => ({ symbol: row[symbolColumn], text: row[textColumn] }))
35
+ }
36
+ return []
37
+ }, [config.dynamicFootnotes, config.formattedData, config.data])
38
+
39
+ // get static footnotes from the config.footnotes
40
+ const staticFootnotes = config.staticFootnotes || []
41
+
42
+ return <Footnotes footnotes={[...apiFootnotes, ...staticFootnotes]} />
43
+ }
44
+
45
+ export default FootnotesStandAlone
@@ -0,0 +1,5 @@
1
+ :is(ul).cove-footnotes {
2
+ :is(li) {
3
+ list-style: none;
4
+ }
5
+ }
@@ -0,0 +1 @@
1
+ export { default } from './Footnotes'
@@ -13,7 +13,8 @@ const breakpoints = [
13
13
  '1280' // xl
14
14
  ]
15
15
 
16
- const os = navigator.userAgent.indexOf('Win') !== -1 ? 'Win' : navigator.userAgent.indexOf('Mac') !== -1 ? 'MacOS' : null
16
+ const os =
17
+ navigator.userAgent.indexOf('Win') !== -1 ? 'Win' : navigator.userAgent.indexOf('Mac') !== -1 ? 'MacOS' : null
17
18
 
18
19
  const Responsive = ({ children, isEditor }) => {
19
20
  const [displayPanel, setDisplayPanel] = useState(false)
@@ -35,6 +36,7 @@ const Responsive = ({ children, isEditor }) => {
35
36
  )
36
37
 
37
38
  const onKeypress = key => {
39
+ if (!isEditor) return key
38
40
  if (key.code === 'KeyL' && key.ctrlKey) setDisplayPanel(display => !display)
39
41
  const viewportCommandKey = os === 'MacOS' ? key.metaKey : key.altKey
40
42
  if (viewportCommandKey) {
@@ -113,7 +115,10 @@ const Responsive = ({ children, isEditor }) => {
113
115
 
114
116
  return (
115
117
  <div className='cove-editor__content' data-grid={displayGrid || null}>
116
- <div className='cove-editor__content-wrap--x' style={viewportPreview ? { maxWidth: viewportPreview + 'px', minWidth: 'unset' } : null}>
118
+ <div
119
+ className='cove-editor__content-wrap--x'
120
+ style={viewportPreview ? { maxWidth: viewportPreview + 'px', minWidth: 'unset' } : null}
121
+ >
117
122
  <div className='cove-editor__content-wrap--y'>
118
123
  <div className='cove-editor-utils__breakpoints--px'>
119
124
  {displayGrid && displayPanel && (
@@ -143,7 +148,8 @@ const Responsive = ({ children, isEditor }) => {
143
148
  <p className={displayGrid ? 'hotkey--active' : null}>G</p>
144
149
  <p className={rotateAnimation ? 'hotkey--active' : null}>R</p>
145
150
  <p className={viewportPreview ? 'hotkey--active' : null}>
146
- {os === 'MacOS' ? <Icon style={{ marginRight: '0.25rem' }} display='command' size={12} /> : 'Alt'} + {viewportPreview ? breakpoints.indexOf(viewportPreview) + 1 : `[1 - ${breakpoints.length}]`}
151
+ {os === 'MacOS' ? <Icon style={{ marginRight: '0.25rem' }} display='command' size={12} /> : 'Alt'} +{' '}
152
+ {viewportPreview ? breakpoints.indexOf(viewportPreview) + 1 : `[1 - ${breakpoints.length}]`}
147
153
  </p>
148
154
  </div>
149
155
  </div>
@@ -161,7 +167,11 @@ const Responsive = ({ children, isEditor }) => {
161
167
  </div>
162
168
  </button>
163
169
  {breakpoints.map((breakpoint, index) => (
164
- <button className={`cove-editor-utils__breakpoints-item${viewportPreview === breakpoint ? ' active' : ''}`} onClick={() => viewportPreviewController(breakpoint)} key={index}>
170
+ <button
171
+ className={`cove-editor-utils__breakpoints-item${viewportPreview === breakpoint ? ' active' : ''}`}
172
+ onClick={() => viewportPreviewController(breakpoint)}
173
+ key={index}
174
+ >
165
175
  {breakpoint}px
166
176
  </button>
167
177
  ))}
@@ -21,21 +21,30 @@ const Sidebar: React.FC<SidebarProps> = props => {
21
21
  const sectionClasses = ['editor-panel', 'cove', 'sidebar']
22
22
  if (!displayPanel) sectionClasses.push('hidden')
23
23
  if (isDashboard) sectionClasses.push('dashboard')
24
- return sectionClasses
24
+ return sectionClasses.join(' ')
25
25
  }
26
26
 
27
27
  const getButtonClasses = () => {
28
28
  const buttonClasses = []
29
29
  if (displayPanel) buttonClasses.push('editor-panel__toggle')
30
30
  if (!displayPanel) buttonClasses.push('collapsed', 'editor-panel__toggle')
31
- return buttonClasses
31
+ return buttonClasses.join(' ')
32
+ }
33
+
34
+ const getTitleClasses = () => {
35
+ const titleClasses = ['editor-panel__title']
36
+ if (!displayPanel) titleClasses.push('collapsed')
37
+ return titleClasses.join(' ')
32
38
  }
33
39
 
34
40
  return (
35
41
  <>
36
- <button className={getButtonClasses().join(' ')} title={displayPanel ? `Collapse Editor` : `Expand Editor`} onClick={onBackClick}></button>
37
- <section className={getSectionClasses().join(' ')}>
38
- <h2 className='editor-panel__title'>{title}</h2>
42
+ {/* mimic the editor panel title to keep the button visible. */}
43
+ <section className='editor-panel__toggle-wrapper p-absolute' style={{ height: '49.75px', width: '350px' }}>
44
+ <button className={getButtonClasses()} title={displayPanel ? `Collapse Editor` : `Expand Editor`} onClick={onBackClick}></button>
45
+ </section>
46
+ <section className={getSectionClasses()}>
47
+ <h2 className={getTitleClasses()}>{title}</h2>
39
48
  <section className='form-container' data-html2canvas-ignore>
40
49
  {children}
41
50
  </section>