@cdc/core 4.24.9 → 4.24.11

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 (106) hide show
  1. package/LICENSE +201 -0
  2. package/assets/icon-combo-chart.svg +1 -0
  3. package/assets/icon-epi-chart.svg +27 -0
  4. package/components/AdvancedEditor/AdvancedEditor.tsx +17 -13
  5. package/components/Alert/components/Alert.tsx +34 -8
  6. package/components/BlurStrokeText.tsx +44 -0
  7. package/components/DataTable/DataTable.tsx +62 -36
  8. package/components/DataTable/DataTableStandAlone.tsx +37 -6
  9. package/components/DataTable/components/ChartHeader.tsx +31 -26
  10. package/components/DataTable/components/MapHeader.tsx +19 -10
  11. package/components/DataTable/components/SortIcon/index.tsx +25 -0
  12. package/components/DataTable/components/SortIcon/sort-icon.css +21 -0
  13. package/{styles/_data-table.scss → components/DataTable/data-table.css} +250 -298
  14. package/components/DataTable/helpers/boxplotCellMatrix.tsx +14 -13
  15. package/components/DataTable/helpers/customSort.ts +11 -15
  16. package/components/DataTable/helpers/getChartCellValue.ts +23 -5
  17. package/components/DataTable/helpers/getDataSeriesColumns.ts +5 -1
  18. package/components/DataTable/helpers/getNewSortBy.ts +35 -0
  19. package/components/DataTable/helpers/tests/customSort.test.ts +52 -0
  20. package/components/DataTable/helpers/tests/getNewSortBy.test.ts +26 -0
  21. package/components/EditorPanel/ColumnsEditor.tsx +81 -36
  22. package/components/EditorPanel/DataTableEditor.tsx +149 -43
  23. package/components/EditorPanel/FieldSetWrapper.tsx +2 -2
  24. package/components/EditorPanel/Inputs.tsx +68 -20
  25. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +25 -7
  26. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +30 -55
  27. package/components/{Filters.tsx → Filters/Filters.tsx} +60 -43
  28. package/components/Filters/helpers/applyQueuedActive.ts +12 -0
  29. package/components/Filters/helpers/getNestedOptions.ts +29 -0
  30. package/components/Filters/helpers/handleSorting.ts +18 -0
  31. package/components/Filters/helpers/tests/applyQueuedActive.test.ts +49 -0
  32. package/components/Filters/helpers/tests/getNestedOptions.test.ts +93 -0
  33. package/components/Filters/helpers/tests/handleSorting.test.ts +68 -0
  34. package/components/Filters/index.ts +5 -0
  35. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +1 -7
  36. package/components/Layout/components/Visualization/visualizations.scss +1 -1
  37. package/components/Legend/Legend.Gradient.tsx +44 -36
  38. package/components/Loader/Loader.tsx +33 -0
  39. package/components/Loader/index.ts +1 -0
  40. package/components/Loader/loader.styles.css +13 -0
  41. package/components/MultiSelect/MultiSelect.tsx +85 -62
  42. package/components/MultiSelect/multiselect.styles.css +10 -7
  43. package/components/NestedDropdown/NestedDropdown.tsx +118 -56
  44. package/components/NestedDropdown/nestedDropdownHelpers.ts +34 -0
  45. package/components/NestedDropdown/nesteddropdown.styles.css +22 -13
  46. package/components/NestedDropdown/tests/nestedDropdownHelpers.test.ts +58 -0
  47. package/components/Table/Table.tsx +102 -34
  48. package/components/Table/components/GroupRow.tsx +1 -1
  49. package/components/_stories/BlurStrokeTest.stories.tsx +27 -0
  50. package/components/_stories/DataTable.stories.tsx +14 -0
  51. package/components/_stories/Filters.stories.tsx +57 -0
  52. package/components/_stories/NestedDropdown.stories.tsx +22 -46
  53. package/components/_stories/_mocks/DataTable/no-data.json +108 -0
  54. package/components/_stories/_mocks/nested-dropdown.json +30 -0
  55. package/components/_stories/styles.scss +0 -1
  56. package/components/ui/Icon.tsx +19 -6
  57. package/components/ui/{Tooltip.jsx → Tooltip.tsx} +38 -14
  58. package/data/colorPalettes.js +107 -10
  59. package/dist/cove-main.css +6080 -0
  60. package/dist/cove-main.css.map +1 -0
  61. package/helpers/DataTransform.ts +2 -1
  62. package/helpers/addValuesToFilters.ts +8 -3
  63. package/helpers/cove/{number.js → number.ts} +62 -27
  64. package/helpers/coveUpdateWorker.ts +6 -7
  65. package/helpers/fetchRemoteData.js +32 -37
  66. package/helpers/formatConfigBeforeSave.ts +17 -1
  67. package/helpers/gatherQueryParams.ts +12 -2
  68. package/helpers/pivotData.ts +52 -11
  69. package/helpers/queryStringUtils.ts +6 -0
  70. package/helpers/tests/gatherQueryParams.test.ts +34 -0
  71. package/helpers/tests/pivotData.test.ts +50 -0
  72. package/helpers/useDataVizClasses.ts +42 -20
  73. package/helpers/ver/4.24.10.ts +47 -0
  74. package/helpers/ver/4.24.9.ts +0 -3
  75. package/helpers/ver/tests/4.24.10.test.ts +45 -0
  76. package/helpers/viewports.ts +9 -0
  77. package/package.json +7 -3
  78. package/styles/_button-section.scss +5 -1
  79. package/styles/_global-variables.scss +20 -2
  80. package/styles/_global.scss +22 -30
  81. package/styles/_reset.scss +2 -26
  82. package/styles/base.scss +0 -1
  83. package/styles/cove-main.scss +6 -0
  84. package/styles/filters.scss +6 -26
  85. package/styles/v2/base/_reset.scss +0 -7
  86. package/styles/v2/components/editor.scss +0 -4
  87. package/styles/v2/components/icon.scss +1 -1
  88. package/styles/v2/components/ui/tooltip.scss +42 -40
  89. package/styles/v2/layout/_component.scss +0 -6
  90. package/styles/v2/layout/index.scss +0 -1
  91. package/types/Axis.ts +4 -0
  92. package/types/BoxPlot.ts +5 -3
  93. package/types/Color.ts +1 -1
  94. package/types/General.ts +1 -0
  95. package/types/Legend.ts +1 -2
  96. package/types/MarkupInclude.ts +1 -0
  97. package/types/Runtime.ts +3 -1
  98. package/types/Series.ts +8 -1
  99. package/types/Table.ts +3 -2
  100. package/types/Visualization.ts +19 -8
  101. package/types/VizFilter.ts +2 -1
  102. package/components/DataTable/components/Icons.tsx +0 -10
  103. package/components/_stories/EditorPanel.stories.tsx +0 -54
  104. package/components/_stories/Layout.Debug.stories.tsx +0 -91
  105. package/components/ui/Select.jsx +0 -30
  106. package/helpers/getGradientLegendWidth.ts +0 -15
@@ -1,15 +1,16 @@
1
- import { useState, useEffect, useRef, useMemo } from 'react'
1
+ import { useState, useEffect, useRef, useMemo, useId } 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
+ import { filterSearchTerm, NestedOptions, ValueTextPair } from './nestedDropdownHelpers'
5
5
 
6
6
  const Options: React.FC<{
7
- currentOptions: (string | number)[]
7
+ subOptions: ValueTextPair[]
8
+ filterIndex: number
8
9
  label: string
9
10
  handleSubGroupSelect: Function
10
11
  userSelectedLabel: string
11
12
  userSearchTerm: string
12
- }> = ({ currentOptions = [], label, handleSubGroupSelect, userSelectedLabel, userSearchTerm }) => {
13
+ }> = ({ subOptions, filterIndex, label, handleSubGroupSelect, userSelectedLabel, userSearchTerm }) => {
13
14
  const [isTierOneExpanded, setIsTierOneExpanded] = useState(true)
14
15
  const checkMark = <>&#10004;</>
15
16
 
@@ -18,7 +19,7 @@ const Options: React.FC<{
18
19
  }, [userSearchTerm])
19
20
 
20
21
  const handleGroupClick = e => {
21
- const leaveExpanded = e.target.className === 'selectable-item' ? true : !isTierOneExpanded
22
+ const leaveExpanded = e.target.className === `selectable-item-${filterIndex}` ? true : !isTierOneExpanded
22
23
  setIsTierOneExpanded(leaveExpanded)
23
24
  }
24
25
 
@@ -26,38 +27,61 @@ const Options: React.FC<{
26
27
  const currentItem = e.target
27
28
  if (e.key === 'ArrowRight') setIsTierOneExpanded(true)
28
29
  else if (e.key === 'ArrowLeft') {
29
- if (currentItem.className === 'selectable-item') currentItem.parentNode.parentNode.focus()
30
+ if (currentItem.className === `selectable-item-${filterIndex}`) currentItem.parentNode.parentNode.focus()
30
31
  setIsTierOneExpanded(false)
31
32
  } else if (e.key === 'Enter') {
32
- currentItem.className === 'selectable-item' ? handleSubGroupSelect(currentItem.dataset.value) : setIsTierOneExpanded(!isTierOneExpanded)
33
+ currentItem.className === `selectable-item-${filterIndex}`
34
+ ? handleSubGroupSelect(currentItem.dataset.value)
35
+ : setIsTierOneExpanded(!isTierOneExpanded)
33
36
  }
34
37
  }
35
38
 
36
39
  return (
37
40
  <>
38
- <li role='treeitem' key={label} tabIndex={0} aria-label={label} onClick={handleGroupClick} onKeyUp={handleKeyUp} className='nested-dropdown-group'>
41
+ <li
42
+ role='treeitem'
43
+ key={label}
44
+ tabIndex={0}
45
+ aria-label={label}
46
+ onClick={handleGroupClick}
47
+ onKeyUp={handleKeyUp}
48
+ className={`nested-dropdown-group-${filterIndex}`}
49
+ >
39
50
  <span className={'font-weight-bold'}>{label} </span>
40
51
  {
41
52
  <span className='list-arrow' aria-hidden='true'>
42
- {isTierOneExpanded ? <Icon display='caretFilledUp' /> : <Icon display='caretFilledDown' />}
53
+ {isTierOneExpanded ? (
54
+ <Icon display='caretFilledUp' alt='arrow pointing up' />
55
+ ) : (
56
+ <Icon display='caretFilledDown' alt='arrow pointing down' />
57
+ )}
43
58
  </span>
44
59
  }
45
- <ul aria-expanded={isTierOneExpanded} role='group' tabIndex={-1} aria-labelledby={label} className={isTierOneExpanded ? '' : 'hide'}>
46
- {currentOptions.map((tierTwo, tierTwoIndex) => {
47
- const regionID = label + tierTwo
60
+ <ul
61
+ aria-expanded={isTierOneExpanded}
62
+ role='group'
63
+ tabIndex={-1}
64
+ aria-labelledby={label}
65
+ className={isTierOneExpanded ? '' : 'hide'}
66
+ >
67
+ {subOptions.map(subGroup => {
68
+ const [value, text] = subGroup
69
+ const subGroupText = text || value
70
+
71
+ const regionID = label + value
48
72
  const isSelected = regionID === userSelectedLabel
49
73
 
50
74
  return (
51
75
  <li
52
76
  key={regionID}
53
- className='selectable-item'
77
+ className={`selectable-item-${filterIndex}`}
54
78
  tabIndex={0}
55
79
  role='treeitem'
56
80
  aria-label={regionID}
57
81
  aria-selected={isSelected}
58
- data-value={tierTwo}
82
+ data-value={value}
59
83
  onClick={e => {
60
- handleSubGroupSelect(tierTwo)
84
+ handleSubGroupSelect(value)
61
85
  }}
62
86
  >
63
87
  {isSelected ? (
@@ -68,7 +92,7 @@ const Options: React.FC<{
68
92
  ''
69
93
  )}
70
94
 
71
- {tierTwo}
95
+ {subGroupText}
72
96
  </li>
73
97
  )
74
98
  })}
@@ -78,44 +102,47 @@ const Options: React.FC<{
78
102
  )
79
103
  }
80
104
 
81
- interface NestedDropdownProps {
105
+ type NestedDropdownProps = {
106
+ activeGroup: string
107
+ activeSubGroup?: string
108
+ filterIndex: number
82
109
  isEditor?: boolean
83
- currentFilter: VizFilter
110
+ isUrlFilter?: boolean
84
111
  listLabel: string
85
- handleSelectedItems: Function
112
+ handleSelectedItems: ([group, subgroup]: [string, string]) => void
113
+ options: NestedOptions
114
+ subGroupingActive?: string
86
115
  }
87
116
 
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 ?? ''
117
+ const NestedDropdown: React.FC<NestedDropdownProps> = ({
118
+ options,
119
+ activeGroup,
120
+ activeSubGroup,
121
+ filterIndex,
122
+ listLabel,
123
+ handleSelectedItems
124
+ }) => {
125
+ const dropdownId = useId()
126
+ const groupFilterActive = activeGroup
127
+ const subGroupFilterActive = activeSubGroup || ''
103
128
 
104
129
  const [userSearchTerm, setUserSearchTerm] = useState('')
105
- const [inputValue, setInputValue] = useState(subGroupFilterActive !== '' ? `${groupFilterActive} - ${subGroupFilterActive}` : 'Select an Option')
130
+ const [inputValue, setInputValue] = useState(
131
+ subGroupFilterActive !== '' ? `${groupFilterActive} - ${subGroupFilterActive}` : ''
132
+ )
106
133
  const [inputHasFocus, setInputHasFocus] = useState(false)
107
134
  const [isListOpened, setIsListOpened] = useState(false)
108
135
 
109
136
  const searchInput = useRef(null)
110
137
  const searchDropdown = useRef(null)
111
138
 
112
- const chooseSelectedSubGroup = (tierOne: string, tierTwo: string) => {
139
+ const chooseSelectedSubGroup = (tierOne: string | number, tierTwo: string | number) => {
113
140
  searchInput.current.focus()
114
141
  const selectedItemValue = `${tierOne} - ${tierTwo}`
115
142
  setUserSearchTerm('')
116
143
  setIsListOpened(false)
117
144
  setInputValue(selectedItemValue)
118
- handleSelectedItems([tierOne, tierTwo])
145
+ handleSelectedItems([String(tierOne), String(tierTwo)])
119
146
  }
120
147
 
121
148
  const handleKeyUp = e => {
@@ -127,7 +154,7 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({ currentFilter, listLabe
127
154
  setIsListOpened(true)
128
155
  // Move focus from Input to top of dropdown
129
156
  Dropdown.firstChild.focus()
130
- } else if (className === 'selectable-item') {
157
+ } else if (className === `selectable-item-${filterIndex}`) {
131
158
  // Move focus to next item on list: next Tier Two item or the next Tier One or SearchInput
132
159
  const itemToFocusOnAfterKeyUp = nextSibling ?? parentNode.parentNode.nextSibling ?? searchInput.current
133
160
  itemToFocusOnAfterKeyUp.focus()
@@ -152,13 +179,14 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({ currentFilter, listLabe
152
179
  // Move focus to last item of the last collapsed Tier Two in dropdown
153
180
  Dropdown.lastChild.lastChild.lastChild.focus()
154
181
  }
155
- } else if (className === 'selectable-item') {
182
+ } else if (className === `selectable-item-${filterIndex}`) {
156
183
  // Move focus to previous Tier Two or Move focus to current Tier One
157
184
  const itemToFocusOnAfterKeyUp = previousSibling ?? parentNode.parentNode
158
185
  itemToFocusOnAfterKeyUp.focus()
159
186
  } else if (previousSibling) {
160
187
  // Move focus to previous collapsed Tier One or Move focus from Tier One to the last of the previous Tier Two's items
161
- const itemToFocusOnAfterKeyUp = previousSibling.lastChild.className === 'hide' ? previousSibling : previousSibling.lastChild.lastChild
188
+ const itemToFocusOnAfterKeyUp =
189
+ previousSibling.lastChild.className === 'hide' ? previousSibling : previousSibling.lastChild.lastChild
162
190
  itemToFocusOnAfterKeyUp.focus()
163
191
  } else {
164
192
  // Move focus from top of the dropdown to Input
@@ -190,11 +218,9 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({ currentFilter, listLabe
190
218
  }
191
219
  }
192
220
 
193
- const filterOptions: OptionsMemo = useMemo(() => {
194
- if (!userSearchTerm) return optsMemo
195
- const newRegex = new RegExp(`^${userSearchTerm}`, 'i')
196
- return optsMemo.filter(([tierOne, tierTwo]) => tierOne.match(newRegex) || tierTwo.some(value => String(value).match(newRegex)))
197
- }, [userSearchTerm])
221
+ const filterOptions = useMemo(() => {
222
+ return filterSearchTerm(userSearchTerm, options)
223
+ }, [userSearchTerm, options])
198
224
 
199
225
  const handleSearchTermChange = e => {
200
226
  const newSearchTerm = e.target.value
@@ -203,11 +229,35 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({ currentFilter, listLabe
203
229
  setInputValue(newSearchTerm)
204
230
  }
205
231
 
232
+ const handleOnBlur = e => {
233
+ if (
234
+ e.relatedTarget === null ||
235
+ ![
236
+ `nested-dropdown-${filterIndex}`,
237
+ `nested-dropdown-group-${filterIndex}`,
238
+ `selectable-item-${filterIndex}`
239
+ ].includes(e.relatedTarget.className)
240
+ ) {
241
+ setInputHasFocus(false)
242
+ setIsListOpened(false)
243
+ }
244
+ }
245
+
206
246
  return (
207
247
  <>
208
- <div id='nested-dropdown-container' className='nested-dropdown' onKeyUp={handleKeyUp}>
248
+ {listLabel && (
249
+ <label className='text-capitalize font-weight-bold' htmlFor={dropdownId}>
250
+ {listLabel}
251
+ </label>
252
+ )}
253
+ <div
254
+ id={dropdownId}
255
+ className={`nested-dropdown nested-dropdown-${filterIndex} ${isListOpened ? 'open-filter' : ''}`}
256
+ onKeyUp={handleKeyUp}
257
+ >
209
258
  <div className='nested-dropdown-input-container' aria-label='searchInput' role='textbox'>
210
259
  <input
260
+ id={`nested-dropdown-${filterIndex}`}
211
261
  className='search-input'
212
262
  ref={searchInput}
213
263
  aria-label='searchInput'
@@ -216,26 +266,38 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({ currentFilter, listLabe
216
266
  tabIndex={0}
217
267
  value={inputValue}
218
268
  onChange={handleSearchTermChange}
219
- placeholder={'Select or type to search'}
269
+ placeholder={'- Select -'}
220
270
  onClick={() => {
221
271
  if (inputHasFocus) setIsListOpened(!isListOpened)
222
272
  }}
223
273
  onFocus={() => setInputHasFocus(true)}
224
- onBlur={() => setInputHasFocus(false)}
274
+ onBlur={e => handleOnBlur(e)}
225
275
  />
226
276
  <span className='list-arrow' aria-hidden={true}>
227
- {isListOpened ? <Icon display='caretFilledUp' /> : <Icon display='caretFilledDown' />}
277
+ <Icon display='caretDown' />
228
278
  </span>
229
279
  </div>
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'}`}>
231
- {filterOptions?.length
232
- ? filterOptions.map(([groupName, options]) => {
280
+ <ul
281
+ role='tree'
282
+ key={listLabel}
283
+ tabIndex={-1}
284
+ aria-labelledby='main-nested-dropdown'
285
+ aria-expanded={isListOpened}
286
+ ref={searchDropdown}
287
+ className={`main-nested-dropdown-container ${isListOpened ? '' : 'hide'}`}
288
+ >
289
+ {filterOptions.length
290
+ ? filterOptions.map(([group, subgroup], index) => {
291
+ const [groupValue, groupText] = group
292
+ const groupTextValue = String(groupText || groupValue)
233
293
  return (
234
294
  <Options
235
- currentOptions={options}
236
- label={groupName}
237
- handleSubGroupSelect={(subGroupValue: string) => {
238
- chooseSelectedSubGroup(groupName, subGroupValue)
295
+ key={groupTextValue + '_' + index}
296
+ subOptions={subgroup}
297
+ filterIndex={filterIndex}
298
+ label={groupTextValue}
299
+ handleSubGroupSelect={subGroupValue => {
300
+ chooseSelectedSubGroup(groupValue, subGroupValue)
239
301
  }}
240
302
  userSelectedLabel={groupFilterActive + subGroupFilterActive}
241
303
  userSearchTerm={userSearchTerm}
@@ -0,0 +1,34 @@
1
+ import _ from 'lodash'
2
+
3
+ export type ValueTextPair = [string | number, string | number | undefined] | [string | number] // [value, text]
4
+
5
+ export type NestedOptions = Array<[ValueTextPair, ValueTextPair[]]>
6
+
7
+ export const filterSearchTerm = (userSearchTerm: string, optsMemo: NestedOptions): NestedOptions => {
8
+ if (userSearchTerm === undefined || userSearchTerm === '') return optsMemo || ([] as NestedOptions)
9
+ const newRegex = new RegExp(`^${userSearchTerm}`, 'i')
10
+ const newOptsMemoTierOneFiltered = optsMemo.filter(([group, subGroups]) => {
11
+ const [groupValue, groupText] = group
12
+ const _groupText = String(groupText || groupValue)
13
+ return (
14
+ _groupText.match(newRegex) ||
15
+ subGroups.some(([value, text]) => {
16
+ const subGroupText = String(text || value)
17
+ return subGroupText.match(newRegex)
18
+ })
19
+ )
20
+ })
21
+
22
+ const filterOptions: NestedOptions = newOptsMemoTierOneFiltered.map(([group, subGroups]) => {
23
+ const [groupValue, groupText] = group
24
+ const _groupText = String(groupText || groupValue)
25
+ if (_groupText.match(newRegex)) return [group, subGroups]
26
+ const newOptions = subGroups.filter(([value, text]) => {
27
+ const subGroupText = text || value
28
+ return String(subGroupText).match(newRegex)
29
+ })
30
+ return [group, newOptions]
31
+ })
32
+
33
+ return filterOptions
34
+ }
@@ -1,5 +1,12 @@
1
+ .cdc-dashboard-inner-container.cove-component__content.cove-dashboard-filters-container:has(
2
+ > .nested-dropdown.open-filter
3
+ )
4
+ :has(> .nested-dropdown-container) {
5
+ z-index: 9;
6
+ }
7
+
1
8
  .nested-dropdown {
2
- .nested-dropdown-group {
9
+ [class^='nested-dropdown-group-'] {
3
10
  list-style: none;
4
11
  }
5
12
 
@@ -8,19 +15,19 @@
8
15
  position: relative;
9
16
  display: inline-block;
10
17
  width: 100%;
18
+ padding: 0;
11
19
  }
12
20
 
13
21
  .main-nested-dropdown-container,
14
22
  .nested-dropdown-input-container {
15
- padding: 7px 15px;
16
- background: #fff;
17
23
  border: 1px solid var(--lightGray);
18
- border-radius: 5px;
19
- width: 325px;
24
+ min-width: 200px;
20
25
  cursor: pointer;
26
+ padding: 0.375rem 0.75rem;
27
+ font-size: 1em;
21
28
  }
22
29
 
23
- .selectable-item {
30
+ [class^='selectable-item-'] {
24
31
  list-style: none;
25
32
  padding-left: 20px;
26
33
  position: relative;
@@ -38,15 +45,16 @@
38
45
  }
39
46
 
40
47
  .nested-dropdown-input-container {
48
+ display: block;
49
+ width: 100%;
50
+ border-radius: 0.25rem;
41
51
  position: relative;
42
52
  & > span.list-arrow {
53
+ color: var(--mediumGray);
43
54
  position: absolute;
44
- font-size: 10px;
45
- top: 4px;
46
- right: 12px;
47
- padding: 9px;
48
- background: #fff;
49
- pointer-events: none;
55
+ top: 9px;
56
+ right: 1px;
57
+ float: right;
50
58
  }
51
59
  }
52
60
 
@@ -55,7 +63,8 @@
55
63
  overflow-y: scroll;
56
64
  position: absolute;
57
65
  z-index: 3;
58
- width: 325px;
66
+ min-width: 200px;
67
+ background: white;
59
68
  }
60
69
 
61
70
  .hide {
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { filterSearchTerm, NestedOptions } from '../nestedDropdownHelpers'
3
+
4
+ describe('filterSearchTerm()', () => {
5
+ const optsMemo: NestedOptions = [
6
+ [['Fruits'], [['Apple'], ['Cantaloupe'], ['Orange'], ['Tomato']]],
7
+ [['Vegetables'], [['Spinach'], ['Broccoli'], ['Carrot'], ['Tomato']]],
8
+ [
9
+ ['Fungi', 'Mushrooms'],
10
+ [['Portobello', 'Mushroom 1'], ['Morel, Mushroom 2'], ['Chanterelle', 'Mushroom 3']]
11
+ ]
12
+ ]
13
+ it('Return the original OptsMemo when there is no search term', () => {
14
+ const result = filterSearchTerm('', optsMemo)
15
+ expect(result).toEqual(optsMemo)
16
+ const result2 = filterSearchTerm(undefined, optsMemo)
17
+ expect(result2).toEqual(optsMemo)
18
+ })
19
+ it('Return the empty array if there is no match to search term', () => {
20
+ const result = filterSearchTerm('Pizza', optsMemo)
21
+ expect(result).toEqual([])
22
+ })
23
+ it('Filter out the top level groupings', () => {
24
+ const result = filterSearchTerm('', optsMemo)
25
+ expect(result).toEqual(optsMemo)
26
+ const result2 = filterSearchTerm('fruit', optsMemo)
27
+ const expectedResult2: NestedOptions = [[['Fruits'], [['Apple'], ['Cantaloupe'], ['Orange'], ['Tomato']]]]
28
+ expect(result2).toEqual(expectedResult2)
29
+ const result3 = filterSearchTerm('VEG', optsMemo)
30
+ const expectedResult3: NestedOptions = [[['Vegetables'], [['Spinach'], ['Broccoli'], ['Carrot'], ['Tomato']]]]
31
+ expect(result3).toEqual(expectedResult3)
32
+ const result4 = filterSearchTerm('mush', optsMemo)
33
+ const expectedResult4: NestedOptions = [
34
+ [
35
+ ['Fungi', 'Mushrooms'],
36
+ [['Portobello', 'Mushroom 1'], ['Morel, Mushroom 2'], ['Chanterelle', 'Mushroom 3']]
37
+ ]
38
+ ]
39
+ expect(result4).toEqual(expectedResult4)
40
+ })
41
+ it('Filter out the subvalues', () => {
42
+ const result = filterSearchTerm('ap', optsMemo)
43
+ const expectedResult: NestedOptions = [[['Fruits'], [['Apple']]]]
44
+ expect(result).toEqual(expectedResult)
45
+ const result2 = filterSearchTerm('ca', optsMemo)
46
+ const expectedResult2: NestedOptions = [
47
+ [['Fruits'], [['Cantaloupe']]],
48
+ [['Vegetables'], [['Carrot']]]
49
+ ]
50
+ expect(result2).toEqual(expectedResult2)
51
+ const result3 = filterSearchTerm('tomato', optsMemo)
52
+ const expectedResult3: NestedOptions = [
53
+ [['Fruits'], [['Tomato']]],
54
+ [['Vegetables'], [['Tomato']]]
55
+ ]
56
+ expect(result3).toEqual(expectedResult3)
57
+ })
58
+ })
@@ -4,9 +4,11 @@ import GroupRow from './components/GroupRow'
4
4
  import { CellMatrix, GroupCellMatrix } from './types/CellMatrix'
5
5
  import { RowType } from './types/RowType'
6
6
  import { PreliminaryDataItem } from '@cdc/chart/src/types/ChartConfig'
7
+ import _ from 'lodash'
7
8
 
8
9
  type TableProps = {
9
10
  childrenMatrix: CellMatrix | GroupCellMatrix
11
+ noData?: boolean
10
12
  tableName: string
11
13
  caption: string
12
14
  stickyHeader?: boolean
@@ -27,46 +29,112 @@ type TableProps = {
27
29
 
28
30
  type Position = 'sticky'
29
31
 
30
- const Table = ({ childrenMatrix, tableName, caption, stickyHeader, headContent, tableOptions, wrapColumns, hasRowType, fontSize, viewport, preliminaryData }: TableProps) => {
32
+ const Table = ({
33
+ childrenMatrix,
34
+ noData,
35
+ tableName,
36
+ caption,
37
+ stickyHeader,
38
+ headContent,
39
+ tableOptions,
40
+ wrapColumns,
41
+ hasRowType,
42
+ fontSize,
43
+ viewport,
44
+ preliminaryData
45
+ }: TableProps) => {
31
46
  const headStyle = stickyHeader ? { position: 'sticky' as Position, top: 0, zIndex: 2 } : {}
32
47
  const isGroupedMatrix = !Array.isArray(childrenMatrix)
33
48
 
34
49
  return (
35
50
  <table {...tableOptions}>
36
51
  <caption className='visually-hidden'>{caption}</caption>
37
- <thead style={headStyle}>{headContent}</thead>
38
- <tbody>
39
- {isGroupedMatrix
40
- ? Object.keys(childrenMatrix).flatMap(groupName => {
41
- let colSpan = 0
42
- const rows = childrenMatrix[groupName].map((row, i) => {
43
- colSpan = row.length
44
- const key = `${tableName}-${groupName}-row-${i}`
45
- return <Row preliminaryData={preliminaryData} key={key} rowKey={key} childRow={row} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} fontSize={fontSize} viewport={viewport} />
46
- })
47
- return [<GroupRow label={groupName} colSpan={colSpan} key={`${tableName}-${groupName}`} />, ...rows]
48
- })
49
- : childrenMatrix.map((childRow, i) => {
50
- let childRowCopy = [...childRow]
51
- let rowType = undefined
52
- if (hasRowType) rowType = childRowCopy.shift()
53
- const key = `${tableName}-row-${i}`
54
- if (rowType === undefined) {
55
- return <Row preliminaryData={preliminaryData} key={key} rowKey={key} childRow={childRow} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} fontSize={fontSize} viewport={viewport} />
56
- } else {
57
- switch (rowType) {
58
- case RowType.row_group:
59
- return <GroupRow label={childRowCopy[0]} colSpan={childRowCopy.length} key={key} />
60
- case RowType.total:
61
- return <Row preliminaryData={preliminaryData} key={key} rowKey={key} childRow={childRowCopy} isTotal={true} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} fontSize={fontSize} viewport={viewport} />
62
- case RowType.row_group_total:
63
- return <GroupRow label={childRowCopy[0]} colSpan={1} key={key} data={childRowCopy.slice(1)} />
64
- default:
65
- return <Row preliminaryData={preliminaryData} key={key} rowKey={key} childRow={childRowCopy} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} fontSize={fontSize} viewport={viewport} />
66
- }
67
- }
68
- })}
69
- </tbody>
52
+ {noData ? (
53
+ <tr>
54
+ <td className='py-5 text-center'>No Data</td>
55
+ </tr>
56
+ ) : (
57
+ <>
58
+ <thead style={headStyle}>{headContent}</thead>
59
+ <tbody>
60
+ {isGroupedMatrix
61
+ ? Object.keys(childrenMatrix).flatMap(groupName => {
62
+ let colSpan = 0
63
+ const rows = childrenMatrix[groupName].map((row, i) => {
64
+ colSpan = row.length
65
+ const key = `${tableName}-${groupName}-row-${i}`
66
+ return (
67
+ <Row
68
+ preliminaryData={preliminaryData}
69
+ key={key}
70
+ rowKey={key}
71
+ childRow={row}
72
+ wrapColumns={wrapColumns}
73
+ cellMinWidth={tableOptions.cellMinWidth}
74
+ fontSize={fontSize}
75
+ viewport={viewport}
76
+ />
77
+ )
78
+ })
79
+ return [<GroupRow label={groupName} colSpan={colSpan} key={`${tableName}-${groupName}`} />, ...rows]
80
+ })
81
+ : childrenMatrix.map((childRow, i) => {
82
+ let childRowCopy = [...childRow]
83
+ let rowType = undefined
84
+ if (hasRowType) rowType = childRowCopy.shift()
85
+ const key = `${tableName}-row-${i}`
86
+ if (rowType === undefined) {
87
+ return (
88
+ <Row
89
+ preliminaryData={preliminaryData}
90
+ key={key}
91
+ rowKey={key}
92
+ childRow={childRow}
93
+ wrapColumns={wrapColumns}
94
+ cellMinWidth={tableOptions.cellMinWidth}
95
+ fontSize={fontSize}
96
+ viewport={viewport}
97
+ />
98
+ )
99
+ } else {
100
+ switch (rowType) {
101
+ case RowType.row_group:
102
+ return <GroupRow label={childRowCopy[0]} colSpan={childRowCopy.length} key={key} />
103
+ case RowType.total:
104
+ return (
105
+ <Row
106
+ preliminaryData={preliminaryData}
107
+ key={key}
108
+ rowKey={key}
109
+ childRow={childRowCopy}
110
+ isTotal={true}
111
+ wrapColumns={wrapColumns}
112
+ cellMinWidth={tableOptions.cellMinWidth}
113
+ fontSize={fontSize}
114
+ viewport={viewport}
115
+ />
116
+ )
117
+ case RowType.row_group_total:
118
+ return <GroupRow label={childRowCopy[0]} colSpan={1} key={key} data={childRowCopy.slice(1)} />
119
+ default:
120
+ return (
121
+ <Row
122
+ preliminaryData={preliminaryData}
123
+ key={key}
124
+ rowKey={key}
125
+ childRow={childRowCopy}
126
+ wrapColumns={wrapColumns}
127
+ cellMinWidth={tableOptions.cellMinWidth}
128
+ fontSize={fontSize}
129
+ viewport={viewport}
130
+ />
131
+ )
132
+ }
133
+ }
134
+ })}
135
+ </tbody>
136
+ </>
137
+ )}
70
138
  </table>
71
139
  )
72
140
  }
@@ -8,7 +8,7 @@ type GroupRowProps = {
8
8
 
9
9
  const GroupRow = ({ label, colSpan, data }: GroupRowProps) => {
10
10
  return (
11
- <tr>
11
+ <tr className='row-group'>
12
12
  <th scope='colgroup' colSpan={colSpan}>
13
13
  {label}
14
14
  </th>
@@ -0,0 +1,27 @@
1
+ import { a } from 'vitest/dist/suite-IbNSsUWN'
2
+ import { BlurStrokeText } from '../BlurStrokeText'
3
+ import { Meta, StoryObj } from '@storybook/react'
4
+
5
+ const meta: Meta<typeof BlurStrokeText> = {
6
+ title: 'Components/Atoms/BlurStrokeText',
7
+ component: BlurStrokeText
8
+ }
9
+
10
+ export default meta
11
+
12
+ type Story = StoryObj<typeof BlurStrokeText>
13
+
14
+ export const Default: Story = {
15
+ render: args => (
16
+ <svg width='300' height='100' style={{ backgroundColor: '#464646' }}>
17
+ <BlurStrokeText {...args}>A feathered stroke option</BlurStrokeText>
18
+ </svg>
19
+ ),
20
+ args: {
21
+ fontSize: 15,
22
+ y: 50,
23
+ x: 50,
24
+ blurRadius: 1,
25
+ disableStroke: false
26
+ }
27
+ }