@cdc/core 4.24.2 → 4.24.4

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 (84) hide show
  1. package/assets/icon-command.svg +3 -0
  2. package/assets/icon-rotate-left.svg +3 -0
  3. package/assets/icon-sankey.svg +1 -0
  4. package/assets/icon-table.svg +1 -0
  5. package/components/AdvancedEditor.jsx +9 -0
  6. package/components/DataTable/DataTable.tsx +37 -13
  7. package/components/DataTable/DataTableStandAlone.tsx +15 -0
  8. package/components/DataTable/components/CellAnchor.tsx +3 -1
  9. package/components/DataTable/components/ChartHeader.tsx +48 -12
  10. package/components/DataTable/components/DataTableEditorPanel.tsx +42 -0
  11. package/components/DataTable/components/ExpandCollapse.tsx +22 -16
  12. package/components/DataTable/components/MapHeader.tsx +10 -5
  13. package/components/DataTable/helpers/chartCellMatrix.tsx +2 -2
  14. package/components/DataTable/helpers/customColumns.ts +4 -2
  15. package/components/DataTable/helpers/getChartCellValue.ts +4 -2
  16. package/components/DataTable/helpers/getDataSeriesColumns.ts +9 -1
  17. package/components/DataTable/helpers/mapCellMatrix.tsx +2 -2
  18. package/components/DataTable/types/TableConfig.ts +7 -7
  19. package/components/EditorPanel/ColumnsEditor.tsx +312 -0
  20. package/components/EditorPanel/DataTableEditor.tsx +42 -27
  21. package/components/Filters.jsx +35 -17
  22. package/components/Layout/components/Responsive.tsx +184 -0
  23. package/components/Layout/components/Sidebar/components/Sidebar.tsx +47 -0
  24. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +902 -0
  25. package/components/Layout/components/Sidebar/index.tsx +3 -0
  26. package/components/Layout/components/Visualization/index.tsx +79 -0
  27. package/components/Layout/components/Visualization/visualizations.scss +33 -0
  28. package/components/Layout/index.tsx +11 -0
  29. package/components/Layout/styles/editor-grid-view.scss +156 -0
  30. package/components/Layout/styles/editor-utils.scss +197 -0
  31. package/components/Layout/styles/editor.scss +144 -0
  32. package/components/LegendCircle.jsx +4 -3
  33. package/components/MediaControls.jsx +1 -1
  34. package/components/MultiSelect/MultiSelect.tsx +39 -20
  35. package/components/MultiSelect/multiselect.styles.css +44 -27
  36. package/components/NestedDropdown/NestedDropdown.tsx +257 -0
  37. package/components/NestedDropdown/index.ts +1 -0
  38. package/components/NestedDropdown/nesteddropdown.styles.css +70 -0
  39. package/components/Table/Table.tsx +8 -6
  40. package/components/Table/components/Row.tsx +6 -2
  41. package/components/Table/types/RowType.ts +3 -0
  42. package/components/Waiting.jsx +11 -1
  43. package/components/_stories/MultiSelect.stories.tsx +10 -1
  44. package/components/_stories/NestedDropdown.stories.tsx +58 -0
  45. package/components/_stories/styles.scss +1 -0
  46. package/components/createBarElement.jsx +120 -0
  47. package/components/elements/ScreenReaderText.tsx +8 -0
  48. package/components/elements/SkipTo.tsx +46 -0
  49. package/components/managers/DataDesigner.tsx +18 -18
  50. package/components/ui/Icon.tsx +9 -1
  51. package/components/ui/Title/Title.scss +7 -1
  52. package/components/ui/Title/index.tsx +3 -3
  53. package/components/ui/Tooltip.jsx +1 -1
  54. package/data/colorPalettes.js +1 -6
  55. package/helpers/cove/accessibility.ts +23 -0
  56. package/helpers/cove/date.ts +19 -0
  57. package/helpers/{coveUpdateWorker.js → coveUpdateWorker.ts} +9 -5
  58. package/helpers/isDomainExternal.js +14 -0
  59. package/helpers/queryStringUtils.js +26 -0
  60. package/helpers/tests/updateFieldFactory.test.ts +89 -0
  61. package/helpers/updateFieldFactory.ts +38 -0
  62. package/helpers/useDataVizClasses.js +7 -7
  63. package/helpers/ver/4.24.3.ts +56 -0
  64. package/package.json +4 -3
  65. package/styles/_data-table.scss +8 -13
  66. package/styles/_global.scss +7 -4
  67. package/styles/_variables.scss +3 -0
  68. package/styles/base.scss +4 -14
  69. package/styles/v2/base/index.scss +1 -1
  70. package/styles/v2/components/ui/tooltip.scss +0 -21
  71. package/types/Axis.ts +3 -0
  72. package/types/BaseVisualizationType.ts +1 -0
  73. package/types/ConfidenceInterval.ts +1 -0
  74. package/types/ConfigureData.ts +8 -0
  75. package/types/DataDescription.ts +9 -0
  76. package/types/Legend.ts +18 -0
  77. package/types/Region.ts +10 -0
  78. package/types/Runtime.ts +2 -0
  79. package/types/Table.ts +2 -1
  80. package/types/UpdateFieldFunc.ts +1 -1
  81. package/types/Visualization.ts +19 -10
  82. package/components/DataTable/components/SkipNav.tsx +0 -7
  83. package/helpers/cove/date.js +0 -9
  84. package/helpers/ver/4.23.js +0 -10
@@ -1,4 +1,5 @@
1
1
  import React, { useEffect, useRef, useState } from 'react'
2
+ import Tooltip from '../ui/Tooltip'
2
3
  import Icon from '../ui/Icon'
3
4
 
4
5
  import './multiselect.styles.css'
@@ -12,14 +13,17 @@ interface Option {
12
13
  interface MultiSelectProps {
13
14
  section?: string
14
15
  subsection?: string
15
- fieldName: string
16
+ fieldName: string | number
16
17
  options: Option[]
17
18
  updateField: UpdateFieldFunc<string[]>
18
19
  label?: string
20
+ selected?: string[]
21
+ limit?: number
19
22
  }
20
23
 
21
- const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection = null, fieldName, label, options, updateField }) => {
22
- const [selectedItems, setSelectedItems] = useState<Option[]>([])
24
+ const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection = null, fieldName, label, options, updateField, selected, limit }) => {
25
+ const preselectedItems = options.filter(opt => selected?.includes(opt.value)).slice(0, limit)
26
+ const [selectedItems, setSelectedItems] = useState<Option[]>(preselectedItems)
23
27
  const [expanded, setExpanded] = useState(false)
24
28
  const multiSelectRef = useRef(null)
25
29
 
@@ -45,13 +49,16 @@ const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection =
45
49
  newItems.map(item => item.value)
46
50
  )
47
51
 
48
- const handleItemSelect = (option: Option) => {
52
+ const handleItemSelect = (option: Option, e = null) => {
53
+ if (e && e.type === 'keyup' && e.key !== 'Enter') return
54
+ if (limit && selectedItems.length >= limit) return
49
55
  const newItems = [...selectedItems, option]
50
56
  setSelectedItems(newItems)
51
57
  update(newItems)
52
58
  }
53
59
 
54
- const handleItemRemove = (option: Option) => {
60
+ const handleItemRemove = (option: Option, e = null) => {
61
+ if (e && e.type === 'keyup' && e.key !== 'Enter') return
55
62
  const newItems = selectedItems.filter(item => item.value !== option.value)
56
63
  setSelectedItems(newItems)
57
64
  update(newItems)
@@ -61,29 +68,41 @@ const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection =
61
68
  return (
62
69
  <div ref={multiSelectRef} className='cove-multiselect'>
63
70
  {label && (
64
- <span id={multiID} className='edit-label cove-input__label'>
71
+ <label id={multiID} className='cove-input__label'>
65
72
  {label}
66
- </span>
73
+ </label>
67
74
  )}
68
75
 
69
- <div aria-labelledby={label ? multiID : undefined} className='selected'>
70
- {selectedItems.map(item => (
71
- <div key={item.value} role='button' tabIndex={0} onClick={() => handleItemRemove(item)} onKeyUp={() => handleItemRemove(item)}>
72
- {item.label}
73
- <button aria-label='Remove' onClick={() => handleItemRemove(item)}>
74
- x
75
- </button>
76
- </div>
77
- ))}
78
- <button aria-label={expanded ? 'Collapse' : 'Expand'} className='expand' onClick={() => setExpanded(!expanded)}>
79
- <Icon display={expanded ? 'caretDown' : 'caretUp'} style={{ cursor: 'pointer' }} />
80
- </button>
76
+ <div className='wrapper'>
77
+ <div className='selected'>
78
+ {selectedItems.map(item => (
79
+ <div key={item.value} aria-labelledby={label ? multiID : undefined} role='button' onClick={() => handleItemRemove(item)} onKeyUp={e => handleItemRemove(item, e)}>
80
+ {item.label}
81
+ <button aria-label='Remove' onClick={() => handleItemRemove(item)}>
82
+ x
83
+ </button>
84
+ </div>
85
+ ))}
86
+ <button aria-label={expanded ? 'Collapse' : 'Expand'} aria-labelledby={label ? multiID : undefined} className='expand' onClick={() => setExpanded(!expanded)}>
87
+ <Icon display={expanded ? 'caretDown' : 'caretUp'} style={{ cursor: 'pointer' }} />
88
+ </button>
89
+ </div>
90
+ {!!limit && (
91
+ <Tooltip style={{ textTransform: 'none' }}>
92
+ <Tooltip.Target>
93
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
94
+ </Tooltip.Target>
95
+ <Tooltip.Content>
96
+ <p>Select up to {limit} items</p>
97
+ </Tooltip.Content>
98
+ </Tooltip>
99
+ )}
81
100
  </div>
82
101
  <ul className={'dropdown' + (expanded ? '' : ' hide')}>
83
102
  {options
84
103
  .filter(option => !selectedItems.find(item => item.value === option.value))
85
104
  .map(option => (
86
- <li className='cove-multiselect-li' key={option.value} role='option' tabIndex={0} onClick={() => handleItemSelect(option)} onKeyUp={() => handleItemSelect(option)}>
105
+ <li className='cove-multiselect-li' key={option.value} role='option' tabIndex={0} onClick={() => handleItemSelect(option)} onKeyUp={e => handleItemSelect(option, e)}>
87
106
  {option.label}
88
107
  </li>
89
108
  ))}
@@ -1,39 +1,56 @@
1
1
  .cove-multiselect {
2
2
  position: relative;
3
- .selected {
4
- border: 1px solid #ccc;
5
- padding: 5px;
6
- min-height: 40px;
7
- :is(button) {
8
- border: none;
9
- background: none;
10
- }
11
- :is(div) {
3
+ .cove-input__label {
4
+ display: block;
5
+ }
6
+ .wrapper {
7
+ display: inline-flex;
8
+ align-items: center;
9
+ .selected {
10
+ border: 1px solid var(--lightGray);
11
+ padding: 5px;
12
+ min-height: 40px;
13
+ min-width: 200px;
12
14
  display: inline-block;
13
- padding: 0 0 0 5px;
14
- margin-right: 5px;
15
- margin-bottom: 2px;
16
- background: #ccc;
15
+ :is(button) {
16
+ border: none;
17
+ background: none;
18
+ }
19
+ :is(div) {
20
+ display: inline-block;
21
+ padding: 0 0 0 5px;
22
+ margin-right: 5px;
23
+ margin-bottom: 2px;
24
+ background: var(--lightGray);
25
+ border-radius: 5px;
26
+ }
27
+ .expand {
28
+ padding: 0 5px;
29
+ border-radius: 5px;
30
+ background: var(--lightGray);
31
+ float: right;
32
+ }
17
33
  border-radius: 5px;
18
34
  }
19
- .expand {
20
- padding: 0 5px;
21
- border-radius: 5px;
22
- background: #ccc;
23
- float: right;
35
+ .cove-tooltip__content {
36
+ position: absolute;
37
+ min-width: 120px;
38
+ :is(p) {
39
+ margin-bottom: 0;
40
+ }
24
41
  }
25
- border-radius: 5px;
26
42
  }
27
43
  .dropdown {
28
44
  background: white;
29
45
  position: absolute;
30
46
  margin-top: 5px;
31
- border: 1px solid #ccc;
32
- padding: 0;
47
+ border: 1px solid var(--lightGray);
48
+ padding-left: 0;
33
49
  min-height: 40px;
34
- overflow: scroll;
35
50
  max-height: 200px;
36
- z-index: 1;
51
+ overflow-y: scroll;
52
+ min-width: 200px;
53
+ z-index: 4;
37
54
  &.hide {
38
55
  display: none;
39
56
  }
@@ -41,10 +58,10 @@
41
58
  :is(li) {
42
59
  cursor: pointer;
43
60
  list-style: none;
44
- padding-left: 10px;
61
+ padding: 0 10px;
45
62
  &:hover {
46
- background: #ccc;
47
- }
63
+ background: var(--lightGray);
64
+ }
48
65
  }
49
66
  }
50
- }
67
+ }
@@ -0,0 +1,257 @@
1
+ import { useState, useEffect, useRef, useMemo } from 'react'
2
+ import './nesteddropdown.styles.css'
3
+ import Icon from '@cdc/core/components/ui/Icon'
4
+
5
+ const Options: React.FC<{
6
+ currentOptions: (string | number)[]
7
+ label: string
8
+ handleSecondTierSelect: Function
9
+ userSelectedTierTwoLabel: string
10
+ userSearchTerm: string
11
+ }> = ({ currentOptions, label, handleSecondTierSelect, userSelectedTierTwoLabel, userSearchTerm }) => {
12
+ const [isTierOneExpanded, setIsTierOneExpanded] = useState(true)
13
+
14
+ const checkMark = <>&#10004;</>
15
+
16
+ useEffect(() => {
17
+ setIsTierOneExpanded(userSearchTerm.length > 0 ? true : isTierOneExpanded)
18
+ }, [userSearchTerm])
19
+
20
+ const handleGroupClick = e => {
21
+ const leaveExpanded = e.target.className === 'selectable-item' ? true : !isTierOneExpanded
22
+ setIsTierOneExpanded(leaveExpanded)
23
+ }
24
+
25
+ const handleKeyUp = e => {
26
+ const currentItem = e.target
27
+ if (e.key === 'ArrowRight') setIsTierOneExpanded(true)
28
+ else if (e.key === 'ArrowLeft') {
29
+ if (currentItem.className === 'selectable-item') currentItem.parentNode.parentNode.focus()
30
+ setIsTierOneExpanded(false)
31
+ } else if (e.key === 'Enter') {
32
+ currentItem.className === 'selectable-item' ? handleSecondTierSelect(currentItem.dataset.value) : setIsTierOneExpanded(!isTierOneExpanded)
33
+ }
34
+ }
35
+
36
+ return (
37
+ <>
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>
40
+ {
41
+ <span className='list-arrow' aria-hidden='true'>
42
+ {isTierOneExpanded ? <Icon display='caretFilledUp' /> : <Icon display='caretFilledDown' />}
43
+ </span>
44
+ }
45
+ <ul aria-expanded={isTierOneExpanded} role='group' tabIndex={-1} aria-labelledby={label} className={isTierOneExpanded ? '' : 'hide'}>
46
+ {currentOptions.map(tierTwo => {
47
+ const regionID = label + tierTwo
48
+ let isSelected = regionID === userSelectedTierTwoLabel
49
+ return (
50
+ <li
51
+ key={regionID}
52
+ className='selectable-item'
53
+ tabIndex={0}
54
+ role='treeitem'
55
+ aria-label={regionID}
56
+ aria-selected={isSelected}
57
+ data-value={tierTwo}
58
+ onClick={e => {
59
+ handleSecondTierSelect(tierTwo)
60
+ }}
61
+ >
62
+ {isSelected ? <span aria-hidden='true'>{checkMark}</span> : ''}
63
+
64
+ {tierTwo}
65
+ </li>
66
+ )
67
+ })}
68
+ </ul>
69
+ </li>
70
+ </>
71
+ )
72
+ }
73
+
74
+ interface NestedDropdownProps {
75
+ data: Record<string, string | number>[]
76
+ tiers: [string, string] // index 0 is the parent index 1 is the child
77
+ listLabel: string
78
+ handleSelectedItems: Function
79
+ }
80
+
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
+ })
93
+
94
+ const [userSelectedTierTwoLabel, setUserSelectedTierTwoLabel] = useState(null)
95
+ const [userSearchTerm, setUserSearchTerm] = useState('')
96
+ const [inputValue, setInputValue] = useState('')
97
+ const [inputHasFocus, setInputHasFocus] = useState(false)
98
+ const [isListOpened, setIsListOpened] = useState(false)
99
+
100
+ const searchInput = useRef(null)
101
+ const searchDropdown = useRef(null)
102
+
103
+ const chooseSelectedSecondTier = (tierOne: string, tierTwo: string) => {
104
+ searchInput.current.focus()
105
+ const selectedItemValue = tierTwo
106
+ setUserSelectedTierTwoLabel(tierOne + tierTwo)
107
+ setUserSearchTerm('')
108
+ setIsListOpened(false)
109
+ setInputValue(selectedItemValue)
110
+ handleSelectedItems(tierOne, tierTwo)
111
+ }
112
+
113
+ const handleKeyUp = e => {
114
+ const { nodeName, className, parentNode, nextSibling, lastChild, previousSibling } = e.target
115
+ const Dropdown = searchDropdown.current
116
+ switch (e.key) {
117
+ case 'ArrowDown': {
118
+ if (nodeName === 'INPUT') {
119
+ setIsListOpened(true)
120
+ // Move focus from Input to top of dropdown
121
+ Dropdown.firstChild.focus()
122
+ } else if (className === 'selectable-item') {
123
+ // Move focus to next item on list: next Tier Two item or the next Tier One or SearchInput
124
+ const itemToFocusOnAfterKeyUp = nextSibling ?? parentNode.parentNode.nextSibling ?? searchInput.current
125
+ itemToFocusOnAfterKeyUp.focus()
126
+ } else if (lastChild.className === 'hide') {
127
+ // If Tier One is collapsed, move to next Tier One or move focus back to the top Input
128
+ const itemToFocusOnAfterKeyUp = nextSibling ?? searchInput.current
129
+ itemToFocusOnAfterKeyUp.focus()
130
+ } else {
131
+ // If Tier One is open, move focus to Tier Two
132
+ lastChild?.firstChild?.focus()
133
+ }
134
+ break
135
+ }
136
+
137
+ case 'ArrowUp': {
138
+ if (nodeName === 'INPUT') {
139
+ setIsListOpened(true)
140
+ if (Dropdown.lastChild.lastChild.className === 'hide') {
141
+ // Move focus from Input textbox to the last collapsed Tier Two in dropdown
142
+ Dropdown.lastChild.focus()
143
+ } else {
144
+ // Move focus to last item of the last collapsed Tier Two in dropdown
145
+ Dropdown.lastChild.lastChild.lastChild.focus()
146
+ }
147
+ } else if (className === 'selectable-item') {
148
+ // Move focus to previous Tier Two or Move focus to current Tier One
149
+ const itemToFocusOnAfterKeyUp = previousSibling ?? parentNode.parentNode
150
+ itemToFocusOnAfterKeyUp.focus()
151
+ } else if (previousSibling) {
152
+ // Move focus to previous collapsed Tier One or Move focus from Tier One to the last of the previous Tier Two's items
153
+ const itemToFocusOnAfterKeyUp = previousSibling.lastChild.className === 'hide' ? previousSibling : previousSibling.lastChild.lastChild
154
+ itemToFocusOnAfterKeyUp.focus()
155
+ } else {
156
+ // Move focus from top of the dropdown to Input
157
+ searchInput?.current.focus()
158
+ }
159
+ break
160
+ }
161
+
162
+ case 'ArrowLeft': {
163
+ if (nodeName === 'INPUT') {
164
+ setIsListOpened(false)
165
+ }
166
+ break
167
+ }
168
+
169
+ case 'ArrowRight': {
170
+ if (nodeName === 'INPUT') {
171
+ setIsListOpened(true)
172
+ }
173
+ break
174
+ }
175
+
176
+ case 'Escape':
177
+ {
178
+ setIsListOpened(false)
179
+ searchInput.current.focus()
180
+ }
181
+ break
182
+ }
183
+ }
184
+
185
+ const filterOptions: Record<string, (string | number)[]> = useMemo(() => {
186
+ if (!userSearchTerm) return optsMemo
187
+ const newOptions: Record<string, (string | number)[]> = {}
188
+ 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
200
+ }, [userSearchTerm])
201
+
202
+ const filterOptionsKeys = Object.keys(filterOptions)
203
+
204
+ const handleSearchTermChange = e => {
205
+ const newSearchTerm = e.target.value
206
+ setIsListOpened(true)
207
+ setUserSearchTerm(newSearchTerm)
208
+ setInputValue(newSearchTerm)
209
+ }
210
+
211
+ return (
212
+ <>
213
+ <div id='nested-dropdown-container' className='nested-dropdown' onKeyUp={handleKeyUp}>
214
+ <div className='nested-dropdown-input-container' aria-label='searchInput' role='textbox'>
215
+ <input
216
+ className='search-input'
217
+ ref={searchInput}
218
+ aria-label='searchInput'
219
+ aria-haspopup='true'
220
+ aria-hidden='false'
221
+ tabIndex={0}
222
+ value={inputValue}
223
+ onChange={handleSearchTermChange}
224
+ placeholder={'Select or type to search'}
225
+ onClick={() => {
226
+ if (inputHasFocus) setIsListOpened(!isListOpened)
227
+ }}
228
+ onFocus={() => setInputHasFocus(true)}
229
+ onBlur={() => setInputHasFocus(false)}
230
+ />
231
+ <span className='list-arrow' aria-hidden={true}>
232
+ {isListOpened ? <Icon display='caretFilledUp' /> : <Icon display='caretFilledDown' />}
233
+ </span>
234
+ </div>
235
+ <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) => {
238
+ return (
239
+ <Options
240
+ currentOptions={filterOptions[tierOne]}
241
+ label={tierOne}
242
+ handleSecondTierSelect={(tierTwo: string) => {
243
+ chooseSelectedSecondTier(tierOne, tierTwo)
244
+ }}
245
+ userSelectedTierTwoLabel={userSelectedTierTwoLabel}
246
+ userSearchTerm={userSearchTerm}
247
+ />
248
+ )
249
+ })
250
+ : 'There are no matching items'}
251
+ </ul>
252
+ </div>
253
+ </>
254
+ )
255
+ }
256
+
257
+ export default NestedDropdown
@@ -0,0 +1 @@
1
+ export { default } from './NestedDropdown'
@@ -0,0 +1,70 @@
1
+ .nested-dropdown {
2
+ li {
3
+ list-style: none;
4
+ cursor: pointer;
5
+
6
+ &:focus {
7
+ border: 2px solid var(--orange);
8
+ }
9
+ }
10
+
11
+ .search-input {
12
+ border: none;
13
+ position: relative;
14
+ display: inline-block;
15
+ width: 100%;
16
+ }
17
+
18
+ .main-nested-dropdown-container,
19
+ .nested-dropdown-input-container {
20
+ padding: 7px 15px;
21
+ background: #fff;
22
+ border: 1px solid var(--lightGray);
23
+ border-radius: 5px;
24
+ width: 325px;
25
+ cursor: pointer;
26
+ }
27
+
28
+ .selectable-item:hover {
29
+ background-color: var(--lightGray);
30
+ }
31
+
32
+ & > div {
33
+ position: relative;
34
+ & > span.list-arrow {
35
+ position: absolute;
36
+ font-size: 10px;
37
+ top: 4px;
38
+ right: 12px;
39
+ padding: 9px;
40
+ background: #fff;
41
+ pointer-events: none;
42
+ }
43
+ }
44
+
45
+ & > ul {
46
+ max-height: 375px;
47
+ overflow-y: scroll;
48
+ position: absolute;
49
+ z-index: 3;
50
+ width: 375px;
51
+
52
+ & > li {
53
+ font-weight: bold;
54
+
55
+ & > ul > li {
56
+ position: relative;
57
+ font-weight: normal;
58
+ white-space: nowrap;
59
+ & > span {
60
+ position: absolute;
61
+ left: -20px;
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ .hide {
68
+ display: none;
69
+ }
70
+ }
@@ -19,12 +19,14 @@ type TableProps = {
19
19
  }
20
20
  wrapColumns?: boolean
21
21
  hasRowType?: boolean // if it has row type then the first column is the row type which will explain how to render the row
22
+ fontSize: 'small' | 'medium' | 'large'
23
+ viewport: 'lg' | 'md' | 'sm' | 'xs' | 'xxs'
22
24
  }
23
25
 
24
26
  type Position = 'sticky'
25
27
 
26
- const Table = ({ childrenMatrix, tableName, caption, stickyHeader, headContent, tableOptions, wrapColumns, hasRowType }: TableProps) => {
27
- const headStyle = stickyHeader ? { position: 'sticky' as Position, top: 0, zIndex: 999 } : {}
28
+ const Table = ({ childrenMatrix, tableName, caption, stickyHeader, headContent, tableOptions, wrapColumns, hasRowType, fontSize, viewport }: TableProps) => {
29
+ const headStyle = stickyHeader ? { position: 'sticky' as Position, top: 0, zIndex: 2 } : {}
28
30
  const isGroupedMatrix = !Array.isArray(childrenMatrix)
29
31
  return (
30
32
  <table {...tableOptions}>
@@ -37,7 +39,7 @@ const Table = ({ childrenMatrix, tableName, caption, stickyHeader, headContent,
37
39
  const rows = childrenMatrix[groupName].map((row, i) => {
38
40
  colSpan = row.length
39
41
  const key = `${tableName}-${groupName}-row-${i}`
40
- return <Row key={key} rowKey={key} childRow={row} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} />
42
+ return <Row key={key} rowKey={key} childRow={row} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} fontSize={fontSize} viewport={viewport} />
41
43
  })
42
44
  return [<GroupRow label={groupName} colSpan={colSpan} key={`${tableName}-${groupName}`} />, ...rows]
43
45
  })
@@ -47,17 +49,17 @@ const Table = ({ childrenMatrix, tableName, caption, stickyHeader, headContent,
47
49
  if (hasRowType) rowType = childRowCopy.shift()
48
50
  const key = `${tableName}-row-${i}`
49
51
  if (rowType === undefined) {
50
- return <Row key={key} rowKey={key} childRow={childRow} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} />
52
+ return <Row key={key} rowKey={key} childRow={childRow} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} fontSize={fontSize} viewport={viewport} />
51
53
  } else {
52
54
  switch (rowType) {
53
55
  case RowType.row_group:
54
56
  return <GroupRow label={childRowCopy[0]} colSpan={childRowCopy.length} key={key} />
55
57
  case RowType.total:
56
- return <Row key={key} rowKey={key} childRow={childRowCopy} isTotal={true} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} />
58
+ return <Row key={key} rowKey={key} childRow={childRowCopy} isTotal={true} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} fontSize={fontSize} viewport={viewport} />
57
59
  case RowType.row_group_total:
58
60
  return <GroupRow label={childRowCopy[0]} colSpan={1} key={key} data={childRowCopy.slice(1)} />
59
61
  default:
60
- return <Row key={key} rowKey={key} childRow={childRowCopy} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} />
62
+ return <Row key={key} rowKey={key} childRow={childRowCopy} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} fontSize={fontSize} viewport={viewport} />
61
63
  }
62
64
  }
63
65
  })}
@@ -7,15 +7,19 @@ type RowProps = {
7
7
  wrapColumns: boolean
8
8
  isTotal?: boolean
9
9
  cellMinWidth?: number
10
+ fontSize: 'small' | 'medium' | 'large'
11
+ viewport: 'lg' | 'md' | 'sm' | 'xs' | 'xxs'
10
12
  }
11
13
 
12
- const Row = ({ childRow, rowKey, wrapColumns, cellMinWidth = 0, isTotal }: RowProps) => {
14
+ const Row = ({ childRow, rowKey, wrapColumns, cellMinWidth = 0, isTotal, fontSize, viewport }: RowProps) => {
13
15
  const whiteSpace = wrapColumns ? 'unset' : 'nowrap'
14
16
  const minWidth = cellMinWidth + 'px'
17
+ const fontSizes = { small: 16, medium: 18, large: 20 }
18
+ const cellFontSize = ['sm', 'xs', 'xxs'].includes(viewport) ? '11px' : `${fontSizes[fontSize]}px`
15
19
  return (
16
20
  <tr>
17
21
  {childRow.map((child, i) => (
18
- <Cell key={rowKey + '__' + i} style={{ whiteSpace, minWidth }} isBold={isTotal}>
22
+ <Cell key={rowKey + '__' + i} style={{ whiteSpace, minWidth, fontSize: cellFontSize }} isBold={isTotal}>
19
23
  {child}
20
24
  </Cell>
21
25
  ))}
@@ -3,3 +3,6 @@ export enum RowType {
3
3
  total = 'total',
4
4
  row_group_total = 'row_group_total'
5
5
  }
6
+
7
+ // The only distinction between total and row_group_total is the way the UI renders it.
8
+ // They function otherwise the same a regular row.
@@ -1,8 +1,18 @@
1
1
  import React from 'react'
2
2
  import '../styles/waiting.scss'
3
3
 
4
+ const styles = {
5
+ position: 'relative',
6
+ height: '100vh',
7
+ width: '100%',
8
+ display: 'flex',
9
+ justifyContent: 'center',
10
+ alignItems: 'center',
11
+ gridArea: 'content'
12
+ }
13
+
4
14
  export default ({ requiredColumns, className }) => (
5
- <section className={className}>
15
+ <section className={className} style={styles}>
6
16
  <section className='waiting-container'>
7
17
  <h3>Configuration Required</h3>
8
18
  <p>
@@ -1,6 +1,7 @@
1
1
  import { Meta, StoryObj } from '@storybook/react'
2
2
 
3
3
  import MultiSelect from '../MultiSelect'
4
+ import { userEvent, within } from '@storybook/testing-library'
4
5
 
5
6
  const meta: Meta<typeof MultiSelect> = {
6
7
  title: 'Components/Molecules/MultiSelect',
@@ -14,10 +15,18 @@ export const Primary: Story = {
14
15
  options: [
15
16
  { value: '1', label: 'One' },
16
17
  { value: '2', label: 'Two' },
17
- { value: '3', label: 'Three' }
18
+ { value: '3', label: 'Three' },
19
+ { value: '4', label: 'This is a really long option' }
18
20
  ],
21
+ selected: ['1', '2'],
22
+ limit: 3,
19
23
  label: 'MultiSelect',
20
24
  updateField: (section, subsection, fieldName, value) => {}
25
+ },
26
+ play: async ({ canvasElement }) => {
27
+ const canvas = within(canvasElement)
28
+ const questionMark = canvas.getByRole('dialog')
29
+ await userEvent.hover(questionMark)
21
30
  }
22
31
  }
23
32