@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.
- package/assets/icon-command.svg +3 -0
- package/assets/icon-rotate-left.svg +3 -0
- package/assets/icon-sankey.svg +1 -0
- package/assets/icon-table.svg +1 -0
- package/components/AdvancedEditor.jsx +9 -0
- package/components/DataTable/DataTable.tsx +37 -13
- package/components/DataTable/DataTableStandAlone.tsx +15 -0
- package/components/DataTable/components/CellAnchor.tsx +3 -1
- package/components/DataTable/components/ChartHeader.tsx +48 -12
- package/components/DataTable/components/DataTableEditorPanel.tsx +42 -0
- package/components/DataTable/components/ExpandCollapse.tsx +22 -16
- package/components/DataTable/components/MapHeader.tsx +10 -5
- package/components/DataTable/helpers/chartCellMatrix.tsx +2 -2
- package/components/DataTable/helpers/customColumns.ts +4 -2
- package/components/DataTable/helpers/getChartCellValue.ts +4 -2
- package/components/DataTable/helpers/getDataSeriesColumns.ts +9 -1
- package/components/DataTable/helpers/mapCellMatrix.tsx +2 -2
- package/components/DataTable/types/TableConfig.ts +7 -7
- package/components/EditorPanel/ColumnsEditor.tsx +312 -0
- package/components/EditorPanel/DataTableEditor.tsx +42 -27
- package/components/Filters.jsx +35 -17
- package/components/Layout/components/Responsive.tsx +184 -0
- package/components/Layout/components/Sidebar/components/Sidebar.tsx +47 -0
- package/components/Layout/components/Sidebar/components/sidebar.styles.scss +902 -0
- package/components/Layout/components/Sidebar/index.tsx +3 -0
- package/components/Layout/components/Visualization/index.tsx +79 -0
- package/components/Layout/components/Visualization/visualizations.scss +33 -0
- package/components/Layout/index.tsx +11 -0
- package/components/Layout/styles/editor-grid-view.scss +156 -0
- package/components/Layout/styles/editor-utils.scss +197 -0
- package/components/Layout/styles/editor.scss +144 -0
- package/components/LegendCircle.jsx +4 -3
- package/components/MediaControls.jsx +1 -1
- package/components/MultiSelect/MultiSelect.tsx +39 -20
- package/components/MultiSelect/multiselect.styles.css +44 -27
- package/components/NestedDropdown/NestedDropdown.tsx +257 -0
- package/components/NestedDropdown/index.ts +1 -0
- package/components/NestedDropdown/nesteddropdown.styles.css +70 -0
- package/components/Table/Table.tsx +8 -6
- package/components/Table/components/Row.tsx +6 -2
- package/components/Table/types/RowType.ts +3 -0
- package/components/Waiting.jsx +11 -1
- package/components/_stories/MultiSelect.stories.tsx +10 -1
- package/components/_stories/NestedDropdown.stories.tsx +58 -0
- package/components/_stories/styles.scss +1 -0
- package/components/createBarElement.jsx +120 -0
- package/components/elements/ScreenReaderText.tsx +8 -0
- package/components/elements/SkipTo.tsx +46 -0
- package/components/managers/DataDesigner.tsx +18 -18
- package/components/ui/Icon.tsx +9 -1
- package/components/ui/Title/Title.scss +7 -1
- package/components/ui/Title/index.tsx +3 -3
- package/components/ui/Tooltip.jsx +1 -1
- package/data/colorPalettes.js +1 -6
- package/helpers/cove/accessibility.ts +23 -0
- package/helpers/cove/date.ts +19 -0
- package/helpers/{coveUpdateWorker.js → coveUpdateWorker.ts} +9 -5
- package/helpers/isDomainExternal.js +14 -0
- package/helpers/queryStringUtils.js +26 -0
- package/helpers/tests/updateFieldFactory.test.ts +89 -0
- package/helpers/updateFieldFactory.ts +38 -0
- package/helpers/useDataVizClasses.js +7 -7
- package/helpers/ver/4.24.3.ts +56 -0
- package/package.json +4 -3
- package/styles/_data-table.scss +8 -13
- package/styles/_global.scss +7 -4
- package/styles/_variables.scss +3 -0
- package/styles/base.scss +4 -14
- package/styles/v2/base/index.scss +1 -1
- package/styles/v2/components/ui/tooltip.scss +0 -21
- package/types/Axis.ts +3 -0
- package/types/BaseVisualizationType.ts +1 -0
- package/types/ConfidenceInterval.ts +1 -0
- package/types/ConfigureData.ts +8 -0
- package/types/DataDescription.ts +9 -0
- package/types/Legend.ts +18 -0
- package/types/Region.ts +10 -0
- package/types/Runtime.ts +2 -0
- package/types/Table.ts +2 -1
- package/types/UpdateFieldFunc.ts +1 -1
- package/types/Visualization.ts +19 -10
- package/components/DataTable/components/SkipNav.tsx +0 -7
- package/helpers/cove/date.js +0 -9
- 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
|
|
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
|
-
<
|
|
71
|
+
<label id={multiID} className='cove-input__label'>
|
|
65
72
|
{label}
|
|
66
|
-
</
|
|
73
|
+
</label>
|
|
67
74
|
)}
|
|
68
75
|
|
|
69
|
-
<div
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
{item.label}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
<
|
|
80
|
-
|
|
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={
|
|
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
|
-
.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
61
|
+
padding: 0 10px;
|
|
45
62
|
&:hover {
|
|
46
|
-
background:
|
|
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 = <>✔</>
|
|
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:
|
|
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
|
))}
|
package/components/Waiting.jsx
CHANGED
|
@@ -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
|
|