@cdc/core 4.24.7 → 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 (66) hide show
  1. package/assets/icon-gear-multi.svg +23 -0
  2. package/components/Alert/components/Alert.styles.css +15 -0
  3. package/components/Alert/components/Alert.tsx +39 -0
  4. package/components/Alert/index.tsx +3 -0
  5. package/components/DataTable/DataTable.tsx +106 -30
  6. package/components/DataTable/helpers/chartCellMatrix.tsx +3 -3
  7. package/components/DataTable/helpers/getChartCellValue.ts +1 -1
  8. package/components/DataTable/helpers/getDataSeriesColumns.ts +2 -2
  9. package/components/DataTable/helpers/mapCellMatrix.tsx +3 -3
  10. package/components/DataTable/types/TableConfig.ts +1 -1
  11. package/components/EditorPanel/Inputs.tsx +13 -4
  12. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +268 -0
  13. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +161 -82
  14. package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +31 -45
  15. package/components/Filters.tsx +223 -180
  16. package/components/Layout/components/Responsive.tsx +14 -4
  17. package/components/Layout/components/Sidebar/components/Sidebar.tsx +14 -5
  18. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +15 -16
  19. package/components/Layout/components/Visualization/index.tsx +7 -1
  20. package/components/Layout/components/Visualization/visualizations.scss +32 -26
  21. package/components/Layout/styles/editor.scss +0 -8
  22. package/components/Legend/Legend.Gradient.tsx +133 -0
  23. package/components/LegendShape.tsx +28 -0
  24. package/components/MultiSelect/MultiSelect.tsx +6 -3
  25. package/components/NestedDropdown/NestedDropdown.tsx +47 -52
  26. package/components/NestedDropdown/nesteddropdown.styles.css +19 -25
  27. package/components/Table/Table.tsx +8 -5
  28. package/components/Table/components/Cell.tsx +2 -2
  29. package/components/Table/components/Row.tsx +25 -7
  30. package/components/_stories/Layout.Debug.stories.tsx +91 -0
  31. package/components/_stories/_mocks/bar-chart-suppressed.json +474 -0
  32. package/components/_stories/styles.scss +13 -1
  33. package/components/createBarElement.jsx +4 -4
  34. package/components/ui/Icon.tsx +21 -14
  35. package/components/ui/Title/Title.scss +0 -8
  36. package/helpers/DataTransform.ts +2 -2
  37. package/helpers/addValuesToFilters.ts +95 -16
  38. package/helpers/cove/accessibility.ts +16 -4
  39. package/helpers/coveUpdateWorker.ts +24 -10
  40. package/helpers/filterVizData.ts +23 -4
  41. package/helpers/formatConfigBeforeSave.ts +7 -2
  42. package/helpers/getGradientLegendWidth.ts +15 -0
  43. package/helpers/getTextWidth.ts +18 -0
  44. package/helpers/scaling.ts +7 -0
  45. package/helpers/tests/addValuesToFilters.test.ts +55 -0
  46. package/helpers/tests/filterVizData.test.ts +31 -0
  47. package/helpers/tests/invertValue.test.ts +35 -0
  48. package/helpers/updatePaletteNames.ts +19 -0
  49. package/helpers/{useDataVizClasses.js → useDataVizClasses.ts} +3 -2
  50. package/helpers/ver/4.24.5.ts +3 -3
  51. package/helpers/ver/4.24.7.ts +34 -3
  52. package/helpers/ver/4.24.9.ts +63 -0
  53. package/helpers/ver/tests/4.24.9.test.ts +22 -0
  54. package/helpers/ver/versionNeedsUpdate.ts +9 -0
  55. package/package.json +3 -3
  56. package/styles/_button-section.scss +1 -1
  57. package/styles/_global.scss +6 -2
  58. package/styles/filters.scss +4 -0
  59. package/types/Axis.ts +3 -0
  60. package/types/Dimensions.ts +1 -0
  61. package/types/General.ts +1 -1
  62. package/types/VizFilter.ts +24 -3
  63. package/components/LegendCircle.jsx +0 -17
  64. package/helpers/updatePaletteNames.js +0 -16
  65. /package/components/{Waiting.jsx → Waiting.tsx} +0 -0
  66. /package/helpers/ver/{4.23.4.ts → 4.24.4.ts} +0 -0
@@ -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>
@@ -11,7 +11,6 @@
11
11
 
12
12
  .cdc-editor .configure .cdc-open-viz-module:not(.type-dashboard) .editor-panel__toggle {
13
13
  position: absolute;
14
- top: 10px;
15
14
  }
16
15
 
17
16
  .cdc-editor .configure .cdc-open-viz-module:not(.type-dashboard) .sidebar {
@@ -25,24 +24,14 @@
25
24
  }
26
25
 
27
26
  .sidebar {
28
- position: fixed;
29
- height: 100vh;
27
+ position: static;
28
+ height: 100%; // take up the whole container
30
29
  top: 0;
31
30
  max-width: 350px;
32
31
  width: 350px;
33
32
  background-color: var(--white);
34
33
  grid-area: panel;
35
34
 
36
- .editor-toggle {
37
- position: fixed !important;
38
- top: 10px !important;
39
- }
40
-
41
- .editor-panel {
42
- position: fixed !important;
43
- top: 0 !important;
44
- }
45
-
46
35
  &.editor-panel--hidden &.hidden {
47
36
  display: none;
48
37
  }
@@ -858,6 +847,10 @@
858
847
  border-bottom: #565656 3px solid;
859
848
  z-index: 3;
860
849
  margin: 0;
850
+
851
+ &.collapsed {
852
+ display: none;
853
+ }
861
854
  }
862
855
 
863
856
  &.type-dashboard {
@@ -866,20 +859,26 @@
866
859
  }
867
860
  }
868
861
 
862
+ .editor-panel__toggle-wrapper {
863
+ position: absolute;
864
+ top: 0;
865
+ }
866
+
869
867
  .editor-panel__toggle {
870
868
  background: #f2f2f2;
871
869
  border-radius: 60px;
872
870
  color: #000;
873
871
  font-size: 1em;
874
872
  border: 0;
875
- position: fixed;
873
+ position: absolute;
876
874
  z-index: 100;
877
875
  transition: 0.1s background;
878
876
  cursor: pointer;
879
877
  width: 25px;
880
878
  height: 25px;
881
- left: 307px;
882
- top: 10px;
879
+ right: 10px;
880
+ top: 50%;
881
+ transform: translateY(-50%);
883
882
  box-shadow: rgba(0, 0, 0, 0.5) 0 1px 2px;
884
883
 
885
884
  &:before {
@@ -17,10 +17,15 @@ type VisualizationWrapper = {
17
17
  }
18
18
 
19
19
  const Visualization: React.FC<VisualizationWrapper> = forwardRef((props, ref) => {
20
- const { config = {}, isEditor = false, currentViewport = 'lg', imageId = '', showEditorPanel = true } = props
20
+ const { config = {}, isEditor = false, currentViewport = 'lg', imageId = '', showEditorPanel = true, className } = props
21
21
 
22
22
  const getWrappingClasses = () => {
23
23
  let classes = ['cdc-open-viz-module', `${currentViewport}`, `font-${config?.fontSize}`, `${config?.theme}`]
24
+
25
+ if (className) {
26
+ classes.push(className)
27
+ }
28
+
24
29
  isEditor && classes.push('spacing-wrapper')
25
30
  isEditor && classes.push('isEditor')
26
31
 
@@ -47,6 +52,7 @@ const Visualization: React.FC<VisualizationWrapper> = forwardRef((props, ref) =>
47
52
  }
48
53
  if (config.type === 'map') {
49
54
  classes.push(`type-map`)
55
+ if (config?.runtime?.editorErrorMessage.length !== 0) classes.push('type-map--has-error')
50
56
  }
51
57
 
52
58
  if (config.type === 'data-bite') {
@@ -1,33 +1,39 @@
1
- .cdc-open-viz-module.isEditor {
2
- overflow: auto;
3
- display: grid;
4
- transition: grid-template-columns 400ms ease-in-out;
5
-
6
- .editor-panel__toggle {
7
- transition: left 400ms ease-in-out;
1
+ .cdc-open-viz-module {
2
+ .cdc-chart-inner-container .cove-component__content {
3
+ padding: 25px 15px !important;
8
4
  }
5
+ &.isEditor {
6
+ overflow: auto;
7
+ display: grid;
8
+ transition: grid-template-columns 400ms ease-in-out;
9
+ min-height: 100vh;
9
10
 
10
- .sidebar {
11
- transition: left 400ms ease-in-out;
12
- }
11
+ .editor-panel__toggle {
12
+ transition: left 400ms ease-in-out;
13
+ }
13
14
 
14
- &.editor-panel--visible {
15
- grid-template-areas: 'panel content';
16
- grid-template-columns: 350px calc(100% - 350px);
17
- }
15
+ .sidebar {
16
+ transition: left 400ms ease-in-out;
17
+ }
18
18
 
19
- &.editor-panel--hidden {
20
- grid-template-areas: 'panel content';
21
- grid-template-columns: 0px 100%;
22
- }
19
+ &.editor-panel--visible {
20
+ grid-template-areas: 'panel content';
21
+ grid-template-columns: 350px calc(100% - 350px);
22
+ overflow: hidden;
23
+ }
24
+
25
+ &.editor-panel--hidden {
26
+ grid-template-areas: 'panel content';
27
+ grid-template-columns: 0px 100%;
28
+ }
23
29
 
24
- .cove-editor__content,
25
- .cove-component__content {
26
- grid-area: content;
27
- position: relative;
28
- left: 0;
29
- width: 100% !important;
30
- padding-left: 0px !important;
31
- grid-area: content;
30
+ .cove-editor__content,
31
+ .cove-component__content {
32
+ grid-area: content;
33
+ position: relative;
34
+ left: 0;
35
+ width: 100% !important;
36
+ grid-area: content;
37
+ }
32
38
  }
33
39
  }
@@ -4,14 +4,6 @@ $mediumGray: #e6e6e6;
4
4
 
5
5
  @import 'editor-grid-view.scss';
6
6
 
7
- .cdc-open-viz-module.isEditor {
8
- background: $mediumGray !important;
9
- }
10
-
11
- // .cdc-open-viz-module .form-container {
12
- // height: 100%;
13
- // }
14
-
15
7
  .cove-editor {
16
8
  display: grid;
17
9
  grid-template-areas: 'panel content';
@@ -0,0 +1,133 @@
1
+ import { Group } from '@visx/group'
2
+ import { Text } from '@visx/text'
3
+ import { type ViewportSize, type MapConfig } from '@cdc/map/src/types/MapConfig'
4
+ import { type ChartConfig } from '@cdc/chart/src/types/ChartConfig'
5
+ import { getGradientLegendWidth } from '@cdc/core/helpers/getGradientLegendWidth'
6
+ import { DimensionsType } from '../../types/Dimensions'
7
+
8
+ type CombinedConfig = MapConfig | ChartConfig
9
+
10
+ interface GradientProps {
11
+ labels: string[]
12
+ colors: string[]
13
+ config: CombinedConfig
14
+ dimensions: DimensionsType
15
+ currentViewport: ViewportSize
16
+ getTextWidth: (text: string, font: string) => string
17
+ }
18
+
19
+ const LegendGradient = ({
20
+ labels,
21
+ colors,
22
+ config,
23
+ dimensions,
24
+ currentViewport,
25
+ getTextWidth
26
+ }: GradientProps): JSX.Element => {
27
+ let [width] = dimensions
28
+
29
+ const legendWidth = getGradientLegendWidth(width, currentViewport)
30
+ const uniqueID = `${config.uid}-${Date.now()}`
31
+
32
+ const numTicks = colors?.length
33
+
34
+ const longestLabel = labels && labels.length > 0 ? labels.reduce((a, b) => (a.length > b.length ? a : b)) : ''
35
+ const boxHeight = 20
36
+ let height = 50
37
+ const margin = 1
38
+
39
+ // configure tick witch and angle
40
+ const textWidth = getTextWidth(longestLabel, `normal 14px sans-serif`)
41
+ const rotationAngle = Number(config.legend.tickRotation) || 0
42
+ // Convert the angle from degrees to radians
43
+ const angleInRadians = rotationAngle * (Math.PI / 180)
44
+ const newHeight = height + Number(textWidth) * Math.sin(angleInRadians)
45
+
46
+ // configre gradient colors
47
+ const stops = colors.map((color, index) => {
48
+ const offset = (index / (colors.length - 1)) * 100
49
+ return <stop key={index} offset={`${offset}%`} style={{ stopColor: color, stopOpacity: 1 }} />
50
+ })
51
+
52
+ // render ticks and labels
53
+ const ticks = labels.map((key, index) => {
54
+ const segmentWidth = legendWidth / numTicks
55
+ const xPositionX = index * segmentWidth + segmentWidth
56
+ const textAnchor = rotationAngle ? 'end' : 'middle'
57
+ const verticalAnchor = rotationAngle ? 'middle' : 'start'
58
+
59
+ return (
60
+ <Group top={margin}>
61
+ <line x1={xPositionX} x2={xPositionX} y1={30} y2={boxHeight} stroke='black' />
62
+ <Text
63
+ angle={-config.legend.tickRotation}
64
+ x={xPositionX}
65
+ y={boxHeight}
66
+ dy={10}
67
+ dx={-segmentWidth / 2}
68
+ fontSize='14'
69
+ textAnchor={textAnchor}
70
+ verticalAnchor={verticalAnchor}
71
+ >
72
+ {key}
73
+ </Text>
74
+ </Group>
75
+ )
76
+ })
77
+ if ((config.type === 'map' && config.legend.position === 'side') || !config.legend.position) {
78
+ return
79
+ }
80
+ if (
81
+ config.type === 'chart' &&
82
+ (config.legend.position === 'left' || config.legend.position === 'right' || !config.legend.position)
83
+ ) {
84
+ return
85
+ }
86
+
87
+ if (config.legend.style === 'gradient') {
88
+ return (
89
+ <svg style={{ overflow: 'visible', width: '100%', marginTop: 10 }} height={newHeight}>
90
+ {/* background border*/}
91
+ <rect
92
+ x={0}
93
+ y={0}
94
+ width={legendWidth + margin * 2}
95
+ height={boxHeight + margin * 2}
96
+ fill='#d3d3d3'
97
+ strokeWidth='0.5'
98
+ />
99
+ {/* Define the gradient */}
100
+ <linearGradient id={`gradient-smooth-${uniqueID}`} x1='0%' y1='0%' x2='100%' y2='0%'>
101
+ {stops}
102
+ </linearGradient>
103
+
104
+ {config.legend.subStyle === 'smooth' && (
105
+ <rect x={1} y={1} width={legendWidth} height={boxHeight} fill={`url(#gradient-smooth-${uniqueID})`} />
106
+ )}
107
+
108
+ {config.legend.subStyle === 'linear blocks' &&
109
+ colors.map((color, index) => {
110
+ const segmentWidth = legendWidth / numTicks
111
+ const xPosition = index * segmentWidth
112
+ return (
113
+ <Group>
114
+ <rect
115
+ key={index}
116
+ x={xPosition}
117
+ y={0}
118
+ width={segmentWidth}
119
+ height={boxHeight}
120
+ fill={color}
121
+ stroke='white'
122
+ strokeWidth='0'
123
+ />
124
+ </Group>
125
+ )
126
+ })}
127
+ {/* Ticks and labels */}
128
+ <g>{ticks}</g>
129
+ </svg>
130
+ )
131
+ }
132
+ }
133
+ export default LegendGradient
@@ -0,0 +1,28 @@
1
+ import React from 'react'
2
+
3
+ interface LegendShapeProps {
4
+ fill: string
5
+ borderColor?: string
6
+ display?: 'inline-block' | 'block' | 'inline'
7
+ shape?: 'circle' | 'square'
8
+ }
9
+
10
+ const LegendShape: React.FC<LegendShapeProps> = props => {
11
+ const { fill, borderColor, display = 'inline-block', shape = 'circle' } = props
12
+ const dimensions = { width: '1em', height: '1em' }
13
+ const marginRight = ['circle', 'square'].includes(shape) ? '5px' : '0'
14
+ const styles = {
15
+ marginRight: marginRight,
16
+ borderRadius: shape === 'circle' ? '50%' : '0px',
17
+ verticalAlign: 'middle',
18
+ display: display,
19
+ height: dimensions.height,
20
+ width: dimensions.width,
21
+ border: borderColor ? `${borderColor} 1px solid` : 'rgba(0,0,0,.3) 1px solid',
22
+ backgroundColor: fill
23
+ }
24
+
25
+ return <span className='legend-item' style={styles} />
26
+ }
27
+
28
+ export default LegendShape
@@ -6,7 +6,7 @@ import './multiselect.styles.css'
6
6
  import { UpdateFieldFunc } from '../../types/UpdateFieldFunc'
7
7
 
8
8
  interface Option {
9
- value: string
9
+ value: string | number
10
10
  label: string
11
11
  }
12
12
 
@@ -17,11 +17,12 @@ interface MultiSelectProps {
17
17
  options: Option[]
18
18
  updateField: UpdateFieldFunc<string[]>
19
19
  label?: string
20
- selected?: string[]
20
+ selected?: (string | number)[]
21
21
  limit?: number
22
+ tooltip?: React.ReactNode
22
23
  }
23
24
 
24
- const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection = null, fieldName, label, options, updateField, selected = [], limit }) => {
25
+ const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection = null, fieldName, label, options, updateField, selected = [], limit, tooltip }) => {
25
26
  const preselectedItems = options.filter(opt => selected.includes(opt.value)).slice(0, limit)
26
27
  const [selectedItems, setSelectedItems] = useState<Option[]>(preselectedItems)
27
28
  const [expanded, setExpanded] = useState(false)
@@ -73,6 +74,8 @@ const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection =
73
74
  </span>
74
75
  )}
75
76
 
77
+ {tooltip && tooltip}
78
+
76
79
  <div className='wrapper'>
77
80
  <div className='selected'>
78
81
  {selectedItems.map(item => (
@@ -1,16 +1,16 @@
1
1
  import { useState, useEffect, useRef, useMemo } from 'react'
2
2
  import './nesteddropdown.styles.css'
3
3
  import Icon from '@cdc/core/components/ui/Icon'
4
+ import { VizFilter } from '../../types/VizFilter'
4
5
 
5
6
  const Options: React.FC<{
6
7
  currentOptions: (string | number)[]
7
8
  label: string
8
- handleSecondTierSelect: Function
9
- userSelectedTierTwoLabel: string
9
+ handleSubGroupSelect: Function
10
+ userSelectedLabel: string
10
11
  userSearchTerm: string
11
- }> = ({ currentOptions, label, handleSecondTierSelect, userSelectedTierTwoLabel, userSearchTerm }) => {
12
+ }> = ({ currentOptions = [], label, handleSubGroupSelect, userSelectedLabel, userSearchTerm }) => {
12
13
  const [isTierOneExpanded, setIsTierOneExpanded] = useState(true)
13
-
14
14
  const checkMark = <>&#10004;</>
15
15
 
16
16
  useEffect(() => {
@@ -29,23 +29,24 @@ const Options: React.FC<{
29
29
  if (currentItem.className === 'selectable-item') currentItem.parentNode.parentNode.focus()
30
30
  setIsTierOneExpanded(false)
31
31
  } else if (e.key === 'Enter') {
32
- currentItem.className === 'selectable-item' ? handleSecondTierSelect(currentItem.dataset.value) : setIsTierOneExpanded(!isTierOneExpanded)
32
+ currentItem.className === 'selectable-item' ? handleSubGroupSelect(currentItem.dataset.value) : setIsTierOneExpanded(!isTierOneExpanded)
33
33
  }
34
34
  }
35
35
 
36
36
  return (
37
37
  <>
38
38
  <li role='treeitem' key={label} tabIndex={0} aria-label={label} onClick={handleGroupClick} onKeyUp={handleKeyUp} className='nested-dropdown-group'>
39
- <span id={label}>{label} </span>
39
+ <span className={'font-weight-bold'}>{label} </span>
40
40
  {
41
41
  <span className='list-arrow' aria-hidden='true'>
42
42
  {isTierOneExpanded ? <Icon display='caretFilledUp' /> : <Icon display='caretFilledDown' />}
43
43
  </span>
44
44
  }
45
45
  <ul aria-expanded={isTierOneExpanded} role='group' tabIndex={-1} aria-labelledby={label} className={isTierOneExpanded ? '' : 'hide'}>
46
- {currentOptions.map(tierTwo => {
46
+ {currentOptions.map((tierTwo, tierTwoIndex) => {
47
47
  const regionID = label + tierTwo
48
- let isSelected = regionID === userSelectedTierTwoLabel
48
+ const isSelected = regionID === userSelectedLabel
49
+
49
50
  return (
50
51
  <li
51
52
  key={regionID}
@@ -56,10 +57,16 @@ const Options: React.FC<{
56
57
  aria-selected={isSelected}
57
58
  data-value={tierTwo}
58
59
  onClick={e => {
59
- handleSecondTierSelect(tierTwo)
60
+ handleSubGroupSelect(tierTwo)
60
61
  }}
61
62
  >
62
- {isSelected ? <span aria-hidden='true'>{checkMark}</span> : ''}
63
+ {isSelected ? (
64
+ <span className='check-mark' aria-hidden='true'>
65
+ {checkMark}
66
+ </span>
67
+ ) : (
68
+ ''
69
+ )}
63
70
 
64
71
  {tierTwo}
65
72
  </li>
@@ -72,42 +79,43 @@ const Options: React.FC<{
72
79
  }
73
80
 
74
81
  interface NestedDropdownProps {
75
- data: Record<string, string | number>[]
76
- tiers: [string, string] // index 0 is the parent index 1 is the child
82
+ isEditor?: boolean
83
+ currentFilter: VizFilter
77
84
  listLabel: string
78
85
  handleSelectedItems: Function
79
86
  }
80
87
 
81
- const NestedDropdown: React.FC<NestedDropdownProps> = ({ data, tiers: [firstTierLabel, secondTierLabel], listLabel, handleSelectedItems }) => {
82
- const optsMemo: Record<string, (string | number)[]> = {}
83
-
84
- data.forEach(value => {
85
- const tierOne = value[firstTierLabel]
86
- const tierTwo = value[secondTierLabel]
87
- if (optsMemo[tierOne]) {
88
- optsMemo[tierOne].push(tierTwo)
89
- } else {
90
- optsMemo[tierOne] = [tierTwo]
91
- }
92
- })
88
+ type OptionsMemo = [string, (string | number)[]][]
89
+
90
+ const NestedDropdown: React.FC<NestedDropdownProps> = ({ currentFilter, listLabel, handleSelectedItems }) => {
91
+ const optsMemo: OptionsMemo = useMemo(() => {
92
+ // keep custom ordered value order
93
+ const values = currentFilter.orderedValues?.filter(value => currentFilter.values.includes(value)) || currentFilter.values
94
+ return values.map(value => {
95
+ if (!currentFilter.subGrouping) return [value, []]
96
+ const { orderedValues, values } = currentFilter.subGrouping.valuesLookup[value]
97
+ const subFilterValues = orderedValues?.filter(value => values.includes(value)) || values
98
+ return [value, subFilterValues]
99
+ })
100
+ }, [currentFilter, currentFilter.subGrouping])
101
+ const groupFilterActive = currentFilter.active
102
+ const subGroupFilterActive = currentFilter.subGrouping?.active ?? ''
93
103
 
94
- const [userSelectedTierTwoLabel, setUserSelectedTierTwoLabel] = useState(null)
95
104
  const [userSearchTerm, setUserSearchTerm] = useState('')
96
- const [inputValue, setInputValue] = useState('')
105
+ const [inputValue, setInputValue] = useState(subGroupFilterActive !== '' ? `${groupFilterActive} - ${subGroupFilterActive}` : 'Select an Option')
97
106
  const [inputHasFocus, setInputHasFocus] = useState(false)
98
107
  const [isListOpened, setIsListOpened] = useState(false)
99
108
 
100
109
  const searchInput = useRef(null)
101
110
  const searchDropdown = useRef(null)
102
111
 
103
- const chooseSelectedSecondTier = (tierOne: string, tierTwo: string) => {
112
+ const chooseSelectedSubGroup = (tierOne: string, tierTwo: string) => {
104
113
  searchInput.current.focus()
105
- const selectedItemValue = tierTwo
106
- setUserSelectedTierTwoLabel(tierOne + tierTwo)
114
+ const selectedItemValue = `${tierOne} - ${tierTwo}`
107
115
  setUserSearchTerm('')
108
116
  setIsListOpened(false)
109
117
  setInputValue(selectedItemValue)
110
- handleSelectedItems(tierOne, tierTwo)
118
+ handleSelectedItems([tierOne, tierTwo])
111
119
  }
112
120
 
113
121
  const handleKeyUp = e => {
@@ -182,25 +190,12 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({ data, tiers: [firstTier
182
190
  }
183
191
  }
184
192
 
185
- const filterOptions: Record<string, (string | number)[]> = useMemo(() => {
193
+ const filterOptions: OptionsMemo = useMemo(() => {
186
194
  if (!userSearchTerm) return optsMemo
187
- const newOptions: Record<string, (string | number)[]> = {}
188
195
  const newRegex = new RegExp(`^${userSearchTerm}`, 'i')
189
- for (const tierOne in optsMemo) {
190
- if (tierOne.match(newRegex)) {
191
- newOptions[tierOne] = [...optsMemo[tierOne]]
192
- } else {
193
- const newSecondTierOptions = optsMemo[tierOne].filter(tierTwo => String(tierTwo).match(newRegex))
194
- if (newSecondTierOptions.length > 0) {
195
- newOptions[tierOne] = newSecondTierOptions
196
- }
197
- }
198
- }
199
- return newOptions
196
+ return optsMemo.filter(([tierOne, tierTwo]) => tierOne.match(newRegex) || tierTwo.some(value => String(value).match(newRegex)))
200
197
  }, [userSearchTerm])
201
198
 
202
- const filterOptionsKeys = Object.keys(filterOptions)
203
-
204
199
  const handleSearchTermChange = e => {
205
200
  const newSearchTerm = e.target.value
206
201
  setIsListOpened(true)
@@ -233,16 +228,16 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({ data, tiers: [firstTier
233
228
  </span>
234
229
  </div>
235
230
  <ul role='tree' key={listLabel} tabIndex={-1} aria-labelledby='main-nested-dropdown' aria-expanded={isListOpened} ref={searchDropdown} className={`main-nested-dropdown-container ${isListOpened ? '' : 'hide'}`}>
236
- {filterOptions && filterOptionsKeys.length > 0
237
- ? filterOptionsKeys.map((tierOne: string) => {
231
+ {filterOptions?.length
232
+ ? filterOptions.map(([groupName, options]) => {
238
233
  return (
239
234
  <Options
240
- currentOptions={filterOptions[tierOne]}
241
- label={tierOne}
242
- handleSecondTierSelect={(tierTwo: string) => {
243
- chooseSelectedSecondTier(tierOne, tierTwo)
235
+ currentOptions={options}
236
+ label={groupName}
237
+ handleSubGroupSelect={(subGroupValue: string) => {
238
+ chooseSelectedSubGroup(groupName, subGroupValue)
244
239
  }}
245
- userSelectedTierTwoLabel={userSelectedTierTwoLabel}
240
+ userSelectedLabel={groupFilterActive + subGroupFilterActive}
246
241
  userSearchTerm={userSearchTerm}
247
242
  />
248
243
  )