@cdc/core 4.24.9 → 4.24.10
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/LICENSE +201 -0
- package/assets/icon-combo-chart.svg +1 -0
- package/assets/icon-epi-chart.svg +27 -0
- package/components/BlurStrokeText.tsx +44 -0
- package/components/DataTable/DataTable.tsx +51 -35
- package/components/DataTable/DataTableStandAlone.tsx +37 -6
- package/components/DataTable/components/ChartHeader.tsx +31 -26
- package/components/DataTable/components/MapHeader.tsx +19 -10
- package/components/DataTable/components/SortIcon/index.tsx +25 -0
- package/components/DataTable/components/SortIcon/sort-icon.css +21 -0
- package/{styles/_data-table.scss → components/DataTable/data-table.css} +268 -298
- package/components/DataTable/helpers/customSort.ts +11 -15
- package/components/DataTable/helpers/getDataSeriesColumns.ts +5 -1
- package/components/DataTable/helpers/getNewSortBy.ts +35 -0
- package/components/DataTable/helpers/tests/customSort.test.ts +52 -0
- package/components/DataTable/helpers/tests/getNewSortBy.test.ts +26 -0
- package/components/EditorPanel/DataTableEditor.tsx +132 -26
- package/components/EditorPanel/Inputs.tsx +42 -4
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +25 -7
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +1 -1
- package/components/{Filters.tsx → Filters/Filters.tsx} +48 -39
- package/components/Filters/helpers/applyQueuedActive.ts +12 -0
- package/components/Filters/helpers/getNestedOptions.ts +29 -0
- package/components/Filters/helpers/handleSorting.ts +18 -0
- package/components/Filters/helpers/tests/applyQueuedActive.test.ts +49 -0
- package/components/Filters/helpers/tests/getNestedOptions.test.ts +93 -0
- package/components/Filters/helpers/tests/handleSorting.test.ts +68 -0
- package/components/Filters/index.ts +5 -0
- package/components/Layout/components/Sidebar/components/sidebar.styles.scss +1 -3
- package/components/Legend/Legend.Gradient.tsx +2 -9
- package/components/Loader/Loader.tsx +33 -0
- package/components/Loader/index.ts +1 -0
- package/components/Loader/loader.styles.css +13 -0
- package/components/NestedDropdown/NestedDropdown.tsx +90 -48
- package/components/NestedDropdown/nestedDropdownHelpers.ts +34 -0
- package/components/NestedDropdown/nesteddropdown.styles.css +7 -0
- package/components/NestedDropdown/tests/nestedDropdownHelpers.test.ts +58 -0
- package/components/Table/components/GroupRow.tsx +1 -1
- package/components/_stories/BlurStrokeTest.stories.tsx +27 -0
- package/components/_stories/NestedDropdown.stories.tsx +22 -46
- package/components/_stories/_mocks/nested-dropdown.json +30 -0
- package/components/_stories/styles.scss +0 -1
- package/components/ui/{Tooltip.jsx → Tooltip.tsx} +38 -14
- package/data/colorPalettes.js +107 -10
- package/dist/cove-main.css +6114 -0
- package/dist/cove-main.css.map +1 -0
- package/helpers/addValuesToFilters.ts +8 -3
- package/helpers/cove/number.js +46 -25
- package/helpers/coveUpdateWorker.ts +6 -7
- package/helpers/formatConfigBeforeSave.ts +16 -1
- package/helpers/gatherQueryParams.ts +12 -2
- package/helpers/pivotData.ts +52 -11
- package/helpers/tests/gatherQueryParams.test.ts +34 -0
- package/helpers/tests/pivotData.test.ts +50 -0
- package/helpers/ver/4.24.10.ts +47 -0
- package/helpers/ver/4.24.9.ts +0 -3
- package/helpers/ver/tests/4.24.10.test.ts +45 -0
- package/helpers/viewports.ts +9 -0
- package/package.json +7 -3
- package/styles/_button-section.scss +4 -0
- package/styles/_global-variables.scss +19 -1
- package/styles/_global.scss +1 -8
- package/styles/_reset.scss +2 -15
- package/styles/base.scss +0 -1
- package/styles/cove-main.scss +6 -0
- package/styles/filters.scss +6 -4
- package/styles/v2/components/ui/tooltip.scss +42 -40
- package/styles/v2/layout/_component.scss +0 -6
- package/styles/v2/layout/index.scss +0 -1
- package/types/Axis.ts +2 -0
- package/types/General.ts +1 -0
- package/types/Table.ts +2 -1
- package/types/Visualization.ts +13 -1
- package/types/VizFilter.ts +2 -1
- package/components/DataTable/components/Icons.tsx +0 -10
- package/components/_stories/EditorPanel.stories.tsx +0 -54
- package/components/_stories/Layout.Debug.stories.tsx +0 -91
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
|
|
3
|
+
export const handleSorting = singleFilter => {
|
|
4
|
+
const singleFilterValues = _.cloneDeep(singleFilter.values)
|
|
5
|
+
if (singleFilter.order === 'cust' && singleFilter.filterStyle !== 'nested-dropdown') {
|
|
6
|
+
singleFilter.values = singleFilter.orderedValues?.length ? singleFilter.orderedValues : singleFilterValues
|
|
7
|
+
return singleFilter
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const sort = (a, b) => {
|
|
11
|
+
const asc = singleFilter.order !== 'desc'
|
|
12
|
+
return String(asc ? a : b).localeCompare(String(asc ? b : a), 'en', { numeric: true })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
singleFilter.values = singleFilterValues.sort(sort)
|
|
16
|
+
|
|
17
|
+
return singleFilter
|
|
18
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { applyQueuedActive } from '../applyQueuedActive'
|
|
3
|
+
import { VIZ_FILTER_STYLE } from '../../Filters'
|
|
4
|
+
import { SharedFilter } from '@cdc/dashboard/src/types/SharedFilter'
|
|
5
|
+
|
|
6
|
+
describe('applyQueuedActive', () => {
|
|
7
|
+
it('should apply queuedActive to active and subGrouping.active for nestedDropdown filter style', () => {
|
|
8
|
+
const sharedFilter: SharedFilter = {
|
|
9
|
+
filterStyle: VIZ_FILTER_STYLE.nestedDropdown,
|
|
10
|
+
queuedActive: ['activeValue', 'subActiveValue'],
|
|
11
|
+
active: null,
|
|
12
|
+
subGrouping: {
|
|
13
|
+
active: null
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
applyQueuedActive(sharedFilter)
|
|
18
|
+
|
|
19
|
+
expect(sharedFilter.active).toBe('activeValue')
|
|
20
|
+
expect(sharedFilter.subGrouping.active).toBe('subActiveValue')
|
|
21
|
+
expect(sharedFilter.queuedActive).toBeUndefined()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should apply queuedActive to active for non-nestedDropdown filter style', () => {
|
|
25
|
+
const sharedFilter: SharedFilter = {
|
|
26
|
+
filterStyle: 'someOtherStyle',
|
|
27
|
+
queuedActive: 'activeValue',
|
|
28
|
+
active: null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
applyQueuedActive(sharedFilter)
|
|
32
|
+
|
|
33
|
+
expect(sharedFilter.active).toBe('activeValue')
|
|
34
|
+
expect(sharedFilter.queuedActive).toBeUndefined()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should handle empty queuedActive for non-nestedDropdown filter style', () => {
|
|
38
|
+
const sharedFilter: SharedFilter = {
|
|
39
|
+
filterStyle: 'someOtherStyle',
|
|
40
|
+
queuedActive: null,
|
|
41
|
+
active: null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
applyQueuedActive(sharedFilter)
|
|
45
|
+
|
|
46
|
+
expect(sharedFilter.active).toBeNull()
|
|
47
|
+
expect(sharedFilter.queuedActive).toBeUndefined()
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { getNestedOptions } from '../getNestedOptions'
|
|
3
|
+
import { SubGrouping } from '../../../../types/VizFilter'
|
|
4
|
+
import { NestedOptions } from '../../../NestedDropdown/nestedDropdownHelpers'
|
|
5
|
+
|
|
6
|
+
describe('getNestedOptions', () => {
|
|
7
|
+
it('should return nested options when orderedValues is not provided', () => {
|
|
8
|
+
const params = {
|
|
9
|
+
values: ['value1', 'value2'],
|
|
10
|
+
subGrouping: null
|
|
11
|
+
}
|
|
12
|
+
const expectedOutput: NestedOptions = [
|
|
13
|
+
[['value1'], []],
|
|
14
|
+
[['value2'], []]
|
|
15
|
+
]
|
|
16
|
+
expect(getNestedOptions(params)).toEqual(expectedOutput)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should return nested options when orderedValues is provided', () => {
|
|
20
|
+
const params = {
|
|
21
|
+
orderedValues: ['value2', 'value1'],
|
|
22
|
+
values: ['value1', 'value2'],
|
|
23
|
+
subGrouping: null
|
|
24
|
+
}
|
|
25
|
+
const expectedOutput: NestedOptions = [
|
|
26
|
+
[['value2'], []],
|
|
27
|
+
[['value1'], []]
|
|
28
|
+
]
|
|
29
|
+
expect(getNestedOptions(params)).toEqual(expectedOutput)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should return nested options when subGrouping is not provided', () => {
|
|
33
|
+
const params = {
|
|
34
|
+
values: ['value1', 'value2'],
|
|
35
|
+
subGrouping: null
|
|
36
|
+
}
|
|
37
|
+
const expectedOutput: NestedOptions = [
|
|
38
|
+
[['value1'], []],
|
|
39
|
+
[['value2'], []]
|
|
40
|
+
]
|
|
41
|
+
expect(getNestedOptions(params)).toEqual(expectedOutput)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should return nested options when subGrouping is provided with nested values', () => {
|
|
45
|
+
const subGrouping: SubGrouping = {
|
|
46
|
+
valuesLookup: {
|
|
47
|
+
value1: {
|
|
48
|
+
orderedValues: ['subValue2', 'subValue1'],
|
|
49
|
+
values: ['subValue1', 'subValue2']
|
|
50
|
+
},
|
|
51
|
+
value2: {
|
|
52
|
+
orderedValues: null,
|
|
53
|
+
values: ['subValue3']
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const params = {
|
|
58
|
+
orderedValues: ['value1', 'value2'],
|
|
59
|
+
values: ['value1', 'value2'],
|
|
60
|
+
subGrouping
|
|
61
|
+
}
|
|
62
|
+
const expectedOutput: NestedOptions = [
|
|
63
|
+
[['value1'], [['subValue2'], ['subValue1']]],
|
|
64
|
+
[['value2'], [['subValue3']]]
|
|
65
|
+
]
|
|
66
|
+
expect(getNestedOptions(params)).toEqual(expectedOutput)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should return nested options when subGrouping is provided without orderedValues', () => {
|
|
70
|
+
const subGrouping: SubGrouping = {
|
|
71
|
+
valuesLookup: {
|
|
72
|
+
value1: {
|
|
73
|
+
orderedValues: null,
|
|
74
|
+
values: ['subValue1', 'subValue2']
|
|
75
|
+
},
|
|
76
|
+
value2: {
|
|
77
|
+
orderedValues: null,
|
|
78
|
+
values: ['subValue3']
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const params = {
|
|
83
|
+
orderedValues: ['value1', 'value2'],
|
|
84
|
+
values: ['value1', 'value2'],
|
|
85
|
+
subGrouping
|
|
86
|
+
}
|
|
87
|
+
const expectedOutput: NestedOptions = [
|
|
88
|
+
[['value1'], [['subValue1'], ['subValue2']]],
|
|
89
|
+
[['value2'], [['subValue3']]]
|
|
90
|
+
]
|
|
91
|
+
expect(getNestedOptions(params)).toEqual(expectedOutput)
|
|
92
|
+
})
|
|
93
|
+
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { handleSorting } from '../handleSorting'
|
|
3
|
+
import _ from 'lodash'
|
|
4
|
+
|
|
5
|
+
describe('handleSorting', () => {
|
|
6
|
+
it('should use orderedValues when order is "cust" and filterStyle is not "nested-dropdown"', () => {
|
|
7
|
+
const singleFilter = {
|
|
8
|
+
values: ['value3', 'value1', 'value2'],
|
|
9
|
+
orderedValues: ['value1', 'value2', 'value3'],
|
|
10
|
+
order: 'cust',
|
|
11
|
+
filterStyle: 'someOtherStyle'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const result = handleSorting(singleFilter)
|
|
15
|
+
|
|
16
|
+
expect(result.values).toEqual(['value1', 'value2', 'value3'])
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should sort values in ascending order by default', () => {
|
|
20
|
+
const singleFilter = {
|
|
21
|
+
values: ['value3', 'value1', 'value2'],
|
|
22
|
+
order: 'asc',
|
|
23
|
+
filterStyle: 'someOtherStyle'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const result = handleSorting(singleFilter)
|
|
27
|
+
|
|
28
|
+
expect(result.values).toEqual(['value1', 'value2', 'value3'])
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should sort values in descending order when order is "desc"', () => {
|
|
32
|
+
const singleFilter = {
|
|
33
|
+
values: ['value3', 'value1', 'value2'],
|
|
34
|
+
order: 'desc',
|
|
35
|
+
filterStyle: 'someOtherStyle'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const result = handleSorting(singleFilter)
|
|
39
|
+
|
|
40
|
+
expect(result.values).toEqual(['value3', 'value2', 'value1'])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should not use orderedValues when filterStyle is "nested-dropdown"', () => {
|
|
44
|
+
const singleFilter = {
|
|
45
|
+
values: ['value3', 'value1', 'value2'],
|
|
46
|
+
orderedValues: ['value1', 'value2', 'value3'],
|
|
47
|
+
order: 'cust',
|
|
48
|
+
filterStyle: 'nested-dropdown'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result = handleSorting(singleFilter)
|
|
52
|
+
|
|
53
|
+
expect(result.values).toEqual(['value1', 'value2', 'value3'])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should handle empty orderedValues when order is "cust"', () => {
|
|
57
|
+
const singleFilter = {
|
|
58
|
+
values: ['value3', 'value1', 'value2'],
|
|
59
|
+
orderedValues: [],
|
|
60
|
+
order: 'cust',
|
|
61
|
+
filterStyle: 'someOtherStyle'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const result = handleSorting(singleFilter)
|
|
65
|
+
|
|
66
|
+
expect(result.values).toEqual(['value3', 'value1', 'value2'])
|
|
67
|
+
})
|
|
68
|
+
})
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
@import '@cdc/core/styles/v2/themes/_color-definitions.scss';
|
|
2
|
-
|
|
3
1
|
.cdc-editor .configure .type-dashboard .sidebar {
|
|
4
2
|
top: 0;
|
|
5
3
|
}
|
|
@@ -210,7 +208,7 @@
|
|
|
210
208
|
|
|
211
209
|
svg {
|
|
212
210
|
width: 60px;
|
|
213
|
-
color:
|
|
211
|
+
color: var(--blue);
|
|
214
212
|
margin-right: 1rem;
|
|
215
213
|
height: 60px; // IE11
|
|
216
214
|
path {
|
|
@@ -3,6 +3,7 @@ import { Text } from '@visx/text'
|
|
|
3
3
|
import { type ViewportSize, type MapConfig } from '@cdc/map/src/types/MapConfig'
|
|
4
4
|
import { type ChartConfig } from '@cdc/chart/src/types/ChartConfig'
|
|
5
5
|
import { getGradientLegendWidth } from '@cdc/core/helpers/getGradientLegendWidth'
|
|
6
|
+
import { getTextWidth } from '../../helpers/getTextWidth'
|
|
6
7
|
import { DimensionsType } from '../../types/Dimensions'
|
|
7
8
|
|
|
8
9
|
type CombinedConfig = MapConfig | ChartConfig
|
|
@@ -13,17 +14,9 @@ interface GradientProps {
|
|
|
13
14
|
config: CombinedConfig
|
|
14
15
|
dimensions: DimensionsType
|
|
15
16
|
currentViewport: ViewportSize
|
|
16
|
-
getTextWidth: (text: string, font: string) => string
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const LegendGradient = ({
|
|
20
|
-
labels,
|
|
21
|
-
colors,
|
|
22
|
-
config,
|
|
23
|
-
dimensions,
|
|
24
|
-
currentViewport,
|
|
25
|
-
getTextWidth
|
|
26
|
-
}: GradientProps): JSX.Element => {
|
|
19
|
+
const LegendGradient = ({ labels, colors, config, dimensions, currentViewport }: GradientProps): JSX.Element => {
|
|
27
20
|
let [width] = dimensions
|
|
28
21
|
|
|
29
22
|
const legendWidth = getGradientLegendWidth(width, currentViewport)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react'
|
|
2
|
+
import './loader.styles.css'
|
|
3
|
+
|
|
4
|
+
type LoaderProps = {
|
|
5
|
+
fullScreen?: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const Spinner = () => (
|
|
9
|
+
<div className='spinner-border text-primary' role='status'>
|
|
10
|
+
<span className='sr-only'>Loading...</span>
|
|
11
|
+
</div>
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const Loader: React.FC<LoaderProps> = ({ fullScreen = false }) => {
|
|
15
|
+
const backgroundRef = useRef(null)
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (backgroundRef?.current) {
|
|
19
|
+
const backgroundHeight = backgroundRef.current.parentElement.clientHeight
|
|
20
|
+
backgroundRef.current.style.height = `${backgroundHeight}px`
|
|
21
|
+
}
|
|
22
|
+
}, [])
|
|
23
|
+
|
|
24
|
+
return fullScreen ? (
|
|
25
|
+
<div ref={backgroundRef} className='cove-loader fullscreen'>
|
|
26
|
+
<Spinner />
|
|
27
|
+
</div>
|
|
28
|
+
) : (
|
|
29
|
+
<Spinner />
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default Loader
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './Loader'
|
|
@@ -1,15 +1,15 @@
|
|
|
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 {
|
|
4
|
+
import { filterSearchTerm, NestedOptions, ValueTextPair } from './nestedDropdownHelpers'
|
|
5
5
|
|
|
6
6
|
const Options: React.FC<{
|
|
7
|
-
|
|
7
|
+
subOptions: ValueTextPair[]
|
|
8
8
|
label: string
|
|
9
9
|
handleSubGroupSelect: Function
|
|
10
10
|
userSelectedLabel: string
|
|
11
11
|
userSearchTerm: string
|
|
12
|
-
}> = ({
|
|
12
|
+
}> = ({ subOptions, label, handleSubGroupSelect, userSelectedLabel, userSearchTerm }) => {
|
|
13
13
|
const [isTierOneExpanded, setIsTierOneExpanded] = useState(true)
|
|
14
14
|
const checkMark = <>✔</>
|
|
15
15
|
|
|
@@ -29,22 +29,45 @@ 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'
|
|
32
|
+
currentItem.className === 'selectable-item'
|
|
33
|
+
? handleSubGroupSelect(currentItem.dataset.value)
|
|
34
|
+
: setIsTierOneExpanded(!isTierOneExpanded)
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
return (
|
|
37
39
|
<>
|
|
38
|
-
<li
|
|
40
|
+
<li
|
|
41
|
+
role='treeitem'
|
|
42
|
+
key={label}
|
|
43
|
+
tabIndex={0}
|
|
44
|
+
aria-label={label}
|
|
45
|
+
onClick={handleGroupClick}
|
|
46
|
+
onKeyUp={handleKeyUp}
|
|
47
|
+
className='nested-dropdown-group'
|
|
48
|
+
>
|
|
39
49
|
<span className={'font-weight-bold'}>{label} </span>
|
|
40
50
|
{
|
|
41
51
|
<span className='list-arrow' aria-hidden='true'>
|
|
42
|
-
{isTierOneExpanded ?
|
|
52
|
+
{isTierOneExpanded ? (
|
|
53
|
+
<Icon display='caretFilledUp' alt='arrow pointing up' />
|
|
54
|
+
) : (
|
|
55
|
+
<Icon display='caretFilledDown' alt='arrow pointing down' />
|
|
56
|
+
)}
|
|
43
57
|
</span>
|
|
44
58
|
}
|
|
45
|
-
<ul
|
|
46
|
-
{
|
|
47
|
-
|
|
59
|
+
<ul
|
|
60
|
+
aria-expanded={isTierOneExpanded}
|
|
61
|
+
role='group'
|
|
62
|
+
tabIndex={-1}
|
|
63
|
+
aria-labelledby={label}
|
|
64
|
+
className={isTierOneExpanded ? '' : 'hide'}
|
|
65
|
+
>
|
|
66
|
+
{subOptions.map(subGroup => {
|
|
67
|
+
const [value, text] = subGroup
|
|
68
|
+
const subGroupText = text || value
|
|
69
|
+
|
|
70
|
+
const regionID = label + value
|
|
48
71
|
const isSelected = regionID === userSelectedLabel
|
|
49
72
|
|
|
50
73
|
return (
|
|
@@ -55,9 +78,9 @@ const Options: React.FC<{
|
|
|
55
78
|
role='treeitem'
|
|
56
79
|
aria-label={regionID}
|
|
57
80
|
aria-selected={isSelected}
|
|
58
|
-
data-value={
|
|
81
|
+
data-value={value}
|
|
59
82
|
onClick={e => {
|
|
60
|
-
handleSubGroupSelect(
|
|
83
|
+
handleSubGroupSelect(value)
|
|
61
84
|
}}
|
|
62
85
|
>
|
|
63
86
|
{isSelected ? (
|
|
@@ -68,7 +91,7 @@ const Options: React.FC<{
|
|
|
68
91
|
''
|
|
69
92
|
)}
|
|
70
93
|
|
|
71
|
-
{
|
|
94
|
+
{subGroupText}
|
|
72
95
|
</li>
|
|
73
96
|
)
|
|
74
97
|
})}
|
|
@@ -78,44 +101,44 @@ const Options: React.FC<{
|
|
|
78
101
|
)
|
|
79
102
|
}
|
|
80
103
|
|
|
81
|
-
|
|
104
|
+
type NestedDropdownProps = {
|
|
105
|
+
activeGroup: string
|
|
106
|
+
activeSubGroup?: string
|
|
82
107
|
isEditor?: boolean
|
|
83
|
-
|
|
108
|
+
isUrlFilter?: boolean
|
|
84
109
|
listLabel: string
|
|
85
|
-
handleSelectedItems:
|
|
110
|
+
handleSelectedItems: ([group, subgroup]: [string, string]) => void
|
|
111
|
+
options: NestedOptions
|
|
112
|
+
subGroupingActive?: string
|
|
86
113
|
}
|
|
87
114
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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 ?? ''
|
|
115
|
+
const NestedDropdown: React.FC<NestedDropdownProps> = ({
|
|
116
|
+
options,
|
|
117
|
+
activeGroup,
|
|
118
|
+
activeSubGroup,
|
|
119
|
+
listLabel,
|
|
120
|
+
handleSelectedItems
|
|
121
|
+
}) => {
|
|
122
|
+
const groupFilterActive = activeGroup
|
|
123
|
+
const subGroupFilterActive = activeSubGroup || ''
|
|
103
124
|
|
|
104
125
|
const [userSearchTerm, setUserSearchTerm] = useState('')
|
|
105
|
-
const [inputValue, setInputValue] = useState(
|
|
126
|
+
const [inputValue, setInputValue] = useState(
|
|
127
|
+
subGroupFilterActive !== '' ? `${groupFilterActive} - ${subGroupFilterActive}` : ''
|
|
128
|
+
)
|
|
106
129
|
const [inputHasFocus, setInputHasFocus] = useState(false)
|
|
107
130
|
const [isListOpened, setIsListOpened] = useState(false)
|
|
108
131
|
|
|
109
132
|
const searchInput = useRef(null)
|
|
110
133
|
const searchDropdown = useRef(null)
|
|
111
134
|
|
|
112
|
-
const chooseSelectedSubGroup = (tierOne: string, tierTwo: string) => {
|
|
135
|
+
const chooseSelectedSubGroup = (tierOne: string | number, tierTwo: string | number) => {
|
|
113
136
|
searchInput.current.focus()
|
|
114
137
|
const selectedItemValue = `${tierOne} - ${tierTwo}`
|
|
115
138
|
setUserSearchTerm('')
|
|
116
139
|
setIsListOpened(false)
|
|
117
140
|
setInputValue(selectedItemValue)
|
|
118
|
-
handleSelectedItems([tierOne, tierTwo])
|
|
141
|
+
handleSelectedItems([String(tierOne), String(tierTwo)])
|
|
119
142
|
}
|
|
120
143
|
|
|
121
144
|
const handleKeyUp = e => {
|
|
@@ -158,7 +181,8 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({ currentFilter, listLabe
|
|
|
158
181
|
itemToFocusOnAfterKeyUp.focus()
|
|
159
182
|
} else if (previousSibling) {
|
|
160
183
|
// 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 =
|
|
184
|
+
const itemToFocusOnAfterKeyUp =
|
|
185
|
+
previousSibling.lastChild.className === 'hide' ? previousSibling : previousSibling.lastChild.lastChild
|
|
162
186
|
itemToFocusOnAfterKeyUp.focus()
|
|
163
187
|
} else {
|
|
164
188
|
// Move focus from top of the dropdown to Input
|
|
@@ -191,10 +215,8 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({ currentFilter, listLabe
|
|
|
191
215
|
}
|
|
192
216
|
|
|
193
217
|
const filterOptions: OptionsMemo = useMemo(() => {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return optsMemo.filter(([tierOne, tierTwo]) => tierOne.match(newRegex) || tierTwo.some(value => String(value).match(newRegex)))
|
|
197
|
-
}, [userSearchTerm])
|
|
218
|
+
return filterSearchTerm(userSearchTerm, options)
|
|
219
|
+
}, [userSearchTerm, options])
|
|
198
220
|
|
|
199
221
|
const handleSearchTermChange = e => {
|
|
200
222
|
const newSearchTerm = e.target.value
|
|
@@ -205,7 +227,12 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({ currentFilter, listLabe
|
|
|
205
227
|
|
|
206
228
|
return (
|
|
207
229
|
<>
|
|
208
|
-
<
|
|
230
|
+
{listLabel && <span className='edit-label column-heading'>{listLabel}</span>}
|
|
231
|
+
<div
|
|
232
|
+
id='nested-dropdown-container'
|
|
233
|
+
className={`nested-dropdown ${isListOpened ? 'open-filter' : ''}`}
|
|
234
|
+
onKeyUp={handleKeyUp}
|
|
235
|
+
>
|
|
209
236
|
<div className='nested-dropdown-input-container' aria-label='searchInput' role='textbox'>
|
|
210
237
|
<input
|
|
211
238
|
className='search-input'
|
|
@@ -216,7 +243,7 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({ currentFilter, listLabe
|
|
|
216
243
|
tabIndex={0}
|
|
217
244
|
value={inputValue}
|
|
218
245
|
onChange={handleSearchTermChange}
|
|
219
|
-
placeholder={'Select
|
|
246
|
+
placeholder={'Select an Option'}
|
|
220
247
|
onClick={() => {
|
|
221
248
|
if (inputHasFocus) setIsListOpened(!isListOpened)
|
|
222
249
|
}}
|
|
@@ -224,18 +251,33 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({ currentFilter, listLabe
|
|
|
224
251
|
onBlur={() => setInputHasFocus(false)}
|
|
225
252
|
/>
|
|
226
253
|
<span className='list-arrow' aria-hidden={true}>
|
|
227
|
-
{isListOpened ?
|
|
254
|
+
{isListOpened ? (
|
|
255
|
+
<Icon display='caretFilledUp' alt='arrow pointing up' />
|
|
256
|
+
) : (
|
|
257
|
+
<Icon display='caretFilledDown' alt='arrow pointing down' />
|
|
258
|
+
)}
|
|
228
259
|
</span>
|
|
229
260
|
</div>
|
|
230
|
-
<ul
|
|
231
|
-
|
|
232
|
-
|
|
261
|
+
<ul
|
|
262
|
+
role='tree'
|
|
263
|
+
key={listLabel}
|
|
264
|
+
tabIndex={-1}
|
|
265
|
+
aria-labelledby='main-nested-dropdown'
|
|
266
|
+
aria-expanded={isListOpened}
|
|
267
|
+
ref={searchDropdown}
|
|
268
|
+
className={`main-nested-dropdown-container ${isListOpened ? '' : 'hide'}`}
|
|
269
|
+
>
|
|
270
|
+
{filterOptions.length
|
|
271
|
+
? filterOptions.map(([group, subgroup], index) => {
|
|
272
|
+
const [groupValue, groupText] = group
|
|
273
|
+
const groupTextValue = String(groupText || groupValue)
|
|
233
274
|
return (
|
|
234
275
|
<Options
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
276
|
+
key={groupTextValue + '_' + index}
|
|
277
|
+
subOptions={subgroup}
|
|
278
|
+
label={groupTextValue}
|
|
279
|
+
handleSubGroupSelect={subGroupValue => {
|
|
280
|
+
chooseSelectedSubGroup(groupValue, subGroupValue)
|
|
239
281
|
}}
|
|
240
282
|
userSelectedLabel={groupFilterActive + subGroupFilterActive}
|
|
241
283
|
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
|
+
}
|