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