@cdc/core 4.23.10 → 4.24.1
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-deviation-bar.svg +1 -0
- package/components/DataTable/DataTable.tsx +223 -0
- package/components/DataTable/components/BoxplotHeader.tsx +16 -0
- package/components/DataTable/components/CellAnchor.tsx +44 -0
- package/components/DataTable/components/ChartHeader.tsx +103 -0
- package/components/DataTable/components/ExpandCollapse.tsx +21 -0
- package/components/DataTable/components/Icons.tsx +10 -0
- package/components/DataTable/components/MapHeader.tsx +56 -0
- package/components/DataTable/components/SkipNav.tsx +7 -0
- package/components/DataTable/helpers/boxplotCellMatrix.tsx +64 -0
- package/components/DataTable/helpers/chartCellMatrix.tsx +92 -0
- package/components/DataTable/helpers/customColumns.ts +25 -0
- package/components/DataTable/helpers/customSort.ts +55 -0
- package/components/DataTable/helpers/getChartCellValue.ts +56 -0
- package/components/DataTable/helpers/getDataSeriesColumns.ts +29 -0
- package/components/DataTable/helpers/getSeriesName.ts +26 -0
- package/components/DataTable/helpers/mapCellMatrix.tsx +56 -0
- package/components/DataTable/helpers/regionCellMatrix.tsx +13 -0
- package/components/DataTable/helpers/standardizeState.js +76 -0
- package/components/DataTable/index.ts +1 -0
- package/components/DataTable/types/TableConfig.ts +52 -0
- package/components/DownloadButton.tsx +29 -0
- package/components/EditorPanel/DataTableEditor.tsx +133 -0
- package/components/EditorPanel/Inputs.tsx +150 -0
- package/components/Filters.jsx +3 -3
- package/components/LegendCircle.jsx +2 -2
- package/components/MediaControls.jsx +1 -1
- package/components/MultiSelect/MultiSelect.tsx +95 -0
- package/components/MultiSelect/index.ts +1 -0
- package/components/MultiSelect/multiselect.styles.css +50 -0
- package/components/Table/Table.tsx +69 -0
- package/components/Table/components/Cell.tsx +9 -0
- package/components/Table/components/GroupRow.tsx +20 -0
- package/components/Table/components/Row.tsx +26 -0
- package/components/Table/index.ts +1 -0
- package/components/Table/types/CellMatrix.ts +4 -0
- package/components/Table/types/RowType.ts +5 -0
- package/components/_stories/DataTable.stories.tsx +103 -0
- package/components/_stories/EditorPanel.stories.tsx +53 -0
- package/components/_stories/Inputs.stories.tsx +37 -0
- package/components/_stories/MultiSelect.stories.tsx +24 -0
- package/components/_stories/Table.stories.tsx +53 -0
- package/components/_stories/_mocks/dashboard_no_filter.json +121 -0
- package/components/_stories/_mocks/example-city-state.json +808 -0
- package/components/_stories/_mocks/row_type.json +42 -0
- package/components/_stories/styles.scss +9 -0
- package/components/inputs/{InputSelect.jsx → InputSelect.tsx} +15 -5
- package/components/managers/{DataDesigner.jsx → DataDesigner.tsx} +103 -94
- package/components/ui/{Icon.jsx → Icon.tsx} +3 -3
- package/components/ui/Title/Title.scss +95 -0
- package/components/ui/Title/index.tsx +34 -0
- package/components/ui/_stories/Title.stories.tsx +21 -0
- package/helpers/DataTransform.ts +75 -20
- package/helpers/cove/string.ts +11 -0
- package/helpers/fetchRemoteData.js +1 -1
- package/helpers/getFileExtension.ts +28 -5
- package/package.json +2 -2
- package/styles/_data-table.scss +3 -0
- package/styles/heading-colors.scss +0 -3
- package/styles/v2/layout/_component.scss +0 -11
- package/types/Axis.ts +41 -0
- package/types/Color.ts +5 -0
- package/types/Column.ts +15 -0
- package/types/ComponentStyles.ts +7 -0
- package/types/ComponentThemes.ts +13 -0
- package/types/EditorColumnProperties.ts +8 -0
- package/types/FilterBehavior.ts +1 -0
- package/types/Runtime.ts +29 -0
- package/types/Series.ts +1 -0
- package/types/Table.ts +18 -0
- package/types/UpdateFieldFunc.ts +1 -0
- package/types/Visualization.ts +21 -0
- package/components/DataTable.jsx +0 -754
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import Tooltip from '@cdc/core/components/ui/Tooltip'
|
|
3
|
+
import Icon from '../ui/Icon'
|
|
4
|
+
import { CheckBox, TextField } from './Inputs'
|
|
5
|
+
import type { Table } from '@cdc/core/types/Table'
|
|
6
|
+
import MultiSelect from '../MultiSelect'
|
|
7
|
+
import { UpdateFieldFunc } from '../../types/UpdateFieldFunc'
|
|
8
|
+
|
|
9
|
+
interface DataTableProps {
|
|
10
|
+
config: {
|
|
11
|
+
table: Table
|
|
12
|
+
visualizationType: string
|
|
13
|
+
}
|
|
14
|
+
updateField: UpdateFieldFunc<string | boolean | string[] | number>
|
|
15
|
+
isDashboard: boolean
|
|
16
|
+
isLoadedFromUrl: boolean
|
|
17
|
+
columns: string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DataTable: React.FC<DataTableProps> = ({ config, updateField, isDashboard, isLoadedFromUrl, columns }) => {
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
<TextField
|
|
24
|
+
value={config.table.label}
|
|
25
|
+
updateField={updateField}
|
|
26
|
+
section='table'
|
|
27
|
+
fieldName='table-label'
|
|
28
|
+
id='tableLabel'
|
|
29
|
+
label='Data Table Title'
|
|
30
|
+
placeholder='Data Table'
|
|
31
|
+
tooltip={
|
|
32
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
33
|
+
<Tooltip.Target>
|
|
34
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
35
|
+
</Tooltip.Target>
|
|
36
|
+
<Tooltip.Content>
|
|
37
|
+
<p>Label is required for Data Table for 508 Compliance</p>
|
|
38
|
+
</Tooltip.Content>
|
|
39
|
+
</Tooltip>
|
|
40
|
+
}
|
|
41
|
+
/>
|
|
42
|
+
<CheckBox
|
|
43
|
+
value={config.table.show}
|
|
44
|
+
fieldName='show'
|
|
45
|
+
label='Show Data Table'
|
|
46
|
+
section='table'
|
|
47
|
+
updateField={updateField}
|
|
48
|
+
className='column-heading'
|
|
49
|
+
tooltip={
|
|
50
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
51
|
+
<Tooltip.Target>
|
|
52
|
+
<Icon display='question' style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }} />
|
|
53
|
+
</Tooltip.Target>
|
|
54
|
+
<Tooltip.Content>
|
|
55
|
+
<p>Hiding the data table may affect accessibility. An alternate form of accessing visualization data is a 508 requirement.</p>
|
|
56
|
+
</Tooltip.Content>
|
|
57
|
+
</Tooltip>
|
|
58
|
+
}
|
|
59
|
+
/>
|
|
60
|
+
{config.visualizationType !== 'Box Plot' && (
|
|
61
|
+
<CheckBox
|
|
62
|
+
value={config.table.showVertical}
|
|
63
|
+
fieldName='showVertical'
|
|
64
|
+
label='Show Vertical Data'
|
|
65
|
+
section='table'
|
|
66
|
+
updateField={updateField}
|
|
67
|
+
className='column-heading'
|
|
68
|
+
tooltip={
|
|
69
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
70
|
+
<Tooltip.Target>
|
|
71
|
+
<Icon display='question' style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }} />
|
|
72
|
+
</Tooltip.Target>
|
|
73
|
+
<Tooltip.Content>
|
|
74
|
+
<p>This will draw the data table with vertical data instead of horizontal.</p>
|
|
75
|
+
</Tooltip.Content>
|
|
76
|
+
</Tooltip>
|
|
77
|
+
}
|
|
78
|
+
/>
|
|
79
|
+
)}
|
|
80
|
+
<TextField value={config.table.indexLabel} section='table' fieldName='indexLabel' label='Index Column Header' updateField={updateField} />
|
|
81
|
+
<TextField
|
|
82
|
+
value={config.table.caption}
|
|
83
|
+
updateField={updateField}
|
|
84
|
+
section='table'
|
|
85
|
+
type='textarea'
|
|
86
|
+
fieldName='caption'
|
|
87
|
+
label='Screen Reader Description'
|
|
88
|
+
placeholder=' Data table'
|
|
89
|
+
tooltip={
|
|
90
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
91
|
+
<Tooltip.Target>
|
|
92
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
93
|
+
</Tooltip.Target>
|
|
94
|
+
<Tooltip.Content>
|
|
95
|
+
<p>Enter a description of the data table to be read by screen readers.</p>
|
|
96
|
+
</Tooltip.Content>
|
|
97
|
+
</Tooltip>
|
|
98
|
+
}
|
|
99
|
+
/>
|
|
100
|
+
<CheckBox value={config.table.limitHeight} section='table' fieldName='limitHeight' label='Limit Table Height' updateField={updateField} />
|
|
101
|
+
<CheckBox
|
|
102
|
+
value={config.table.customTableConfig}
|
|
103
|
+
fieldName='customTableConfig'
|
|
104
|
+
label='Customize Table Config'
|
|
105
|
+
section='table'
|
|
106
|
+
updateField={updateField}
|
|
107
|
+
tooltip={
|
|
108
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
109
|
+
<Tooltip.Target>
|
|
110
|
+
<Icon display='question' style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }} />
|
|
111
|
+
</Tooltip.Target>
|
|
112
|
+
<Tooltip.Content>
|
|
113
|
+
<p>This will display all available columns in the data set. It will not show any columns where all of the column cells are null.</p>
|
|
114
|
+
</Tooltip.Content>
|
|
115
|
+
</Tooltip>
|
|
116
|
+
}
|
|
117
|
+
/>
|
|
118
|
+
{config.table.customTableConfig && <MultiSelect options={columns.map(c => ({ label: c, value: c }))} fieldName='excludeColumns' label='Exclude Columns' section='table' updateField={updateField} />}
|
|
119
|
+
{config.table.limitHeight && <TextField value={config.table.height} fieldName='height' label='Data Table Height' type='number' min={0} max={500} placeholder='Height(px)' updateField={updateField} />}
|
|
120
|
+
<CheckBox value={config.table.expanded} fieldName='expanded' label='Expanded by Default' section='table' updateField={updateField} />
|
|
121
|
+
{isDashboard && <CheckBox value={config.table.showDataTableLink} fieldName='showDataTableLink' label='Show Data Table Name & Link' section='table' updateField={updateField} />}
|
|
122
|
+
{isLoadedFromUrl && <CheckBox value={config.table.showDownloadUrl} fieldName='showDownloadUrl' label='Show URL to Automatically Updated Data' section='table' updateField={updateField} />}
|
|
123
|
+
<CheckBox value={config.table.download} fieldName='download' label='Show Download CSV Link' section='table' updateField={updateField} />
|
|
124
|
+
<CheckBox value={config.table.showDownloadImgButton} fieldName='showDownloadImgButton' label='Display Image Button' section='table' updateField={updateField} />
|
|
125
|
+
<label>
|
|
126
|
+
<span className='edit-label column-heading'>Table Cell Min Width</span>
|
|
127
|
+
<input type='number' value={config.table.cellMinWidth ? config.table.cellMinWidth : 0} onChange={e => updateField('table', null, 'cellMinWidth', e.target.value)} />
|
|
128
|
+
</label>
|
|
129
|
+
</>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export default DataTable
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { memo, useEffect, useState } from 'react'
|
|
2
|
+
import { useDebounce } from 'use-debounce'
|
|
3
|
+
|
|
4
|
+
export type Input = {
|
|
5
|
+
label: string
|
|
6
|
+
tooltip?: any
|
|
7
|
+
section?: any
|
|
8
|
+
placeholder?: string
|
|
9
|
+
subsection?: any
|
|
10
|
+
updateField?: Function
|
|
11
|
+
fieldName?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type TextFieldProps = {
|
|
15
|
+
className?: string
|
|
16
|
+
value: string | number
|
|
17
|
+
type?: 'text' | 'number' | 'textarea' | 'date'
|
|
18
|
+
min?: number
|
|
19
|
+
max?: number
|
|
20
|
+
i?: number
|
|
21
|
+
id?: string
|
|
22
|
+
} & Input
|
|
23
|
+
|
|
24
|
+
export type CheckboxProps = {
|
|
25
|
+
value?: boolean
|
|
26
|
+
min?: number
|
|
27
|
+
i?: number
|
|
28
|
+
className?: string
|
|
29
|
+
} & Input
|
|
30
|
+
|
|
31
|
+
export type SelectProps = {
|
|
32
|
+
value?: string
|
|
33
|
+
options?: string[]
|
|
34
|
+
required?: boolean
|
|
35
|
+
initial?: string
|
|
36
|
+
|
|
37
|
+
// all other props
|
|
38
|
+
[x: string]: any
|
|
39
|
+
} & Input
|
|
40
|
+
|
|
41
|
+
const TextField = memo((props: TextFieldProps) => {
|
|
42
|
+
const { label, tooltip, section = null, subsection = null, fieldName, updateField, value: stateValue, type = 'text', i = null, min = null, ...attributes } = props
|
|
43
|
+
const [value, setValue] = useState(stateValue)
|
|
44
|
+
const [debouncedValue] = useDebounce(value, 500)
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if ('string' === typeof debouncedValue && stateValue !== debouncedValue) {
|
|
48
|
+
updateField(section, subsection, fieldName, debouncedValue, i)
|
|
49
|
+
}
|
|
50
|
+
}, [debouncedValue])
|
|
51
|
+
|
|
52
|
+
let name = subsection ? `${section}-${subsection}-${fieldName}` : `${section}-${subsection}-${fieldName}`
|
|
53
|
+
|
|
54
|
+
const onChange = e => {
|
|
55
|
+
if ('number' !== type || min === null) {
|
|
56
|
+
setValue(e.target.value)
|
|
57
|
+
} else {
|
|
58
|
+
if (!e.target.value || min <= parseFloat(e.target.value)) {
|
|
59
|
+
setValue(e.target.value)
|
|
60
|
+
} else {
|
|
61
|
+
setValue(min.toString())
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let formElement = <input type='text' name={name} onChange={onChange} {...attributes} value={value} />
|
|
67
|
+
|
|
68
|
+
if ('textarea' === type) {
|
|
69
|
+
formElement = <textarea name={name} onChange={onChange} {...attributes} value={value}></textarea>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if ('number' === type) {
|
|
73
|
+
formElement = <input type='number' name={name} onChange={onChange} {...attributes} value={value} />
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if ('date' === type) {
|
|
77
|
+
formElement = <input type='date' name={name} onChange={onChange} {...attributes} value={value} />
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<label>
|
|
82
|
+
<span className='edit-label column-heading'>
|
|
83
|
+
{label}
|
|
84
|
+
{tooltip}
|
|
85
|
+
</span>
|
|
86
|
+
{formElement}
|
|
87
|
+
</label>
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const CheckBox = memo((props: CheckboxProps) => {
|
|
92
|
+
const { label, value, fieldName, section = null, subsection = null, tooltip, updateField, ...attributes } = props
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<label className='checkbox column-heading'>
|
|
96
|
+
<input
|
|
97
|
+
type='checkbox'
|
|
98
|
+
name={fieldName}
|
|
99
|
+
checked={value}
|
|
100
|
+
onChange={e => {
|
|
101
|
+
updateField(section, subsection, fieldName, !value)
|
|
102
|
+
}}
|
|
103
|
+
{...attributes}
|
|
104
|
+
/>
|
|
105
|
+
<span className='edit-label'>
|
|
106
|
+
{label}
|
|
107
|
+
{tooltip}
|
|
108
|
+
</span>
|
|
109
|
+
</label>
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const Select = memo((props: SelectProps) => {
|
|
114
|
+
const { label, value, options, fieldName, section = null, subsection = null, required = false, tooltip, updateField, initial: initialValue, ...attributes } = props
|
|
115
|
+
let optionsJsx = options.map((optionName, index) => (
|
|
116
|
+
<option value={optionName} key={index}>
|
|
117
|
+
{optionName}
|
|
118
|
+
</option>
|
|
119
|
+
))
|
|
120
|
+
|
|
121
|
+
if (initialValue) {
|
|
122
|
+
optionsJsx.unshift(
|
|
123
|
+
<option value='' key='initial'>
|
|
124
|
+
{initialValue}
|
|
125
|
+
</option>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<label>
|
|
131
|
+
<span className='edit-label'>
|
|
132
|
+
{label}
|
|
133
|
+
{tooltip}
|
|
134
|
+
</span>
|
|
135
|
+
<select
|
|
136
|
+
className={required && !value ? 'warning' : ''}
|
|
137
|
+
name={fieldName}
|
|
138
|
+
value={value}
|
|
139
|
+
onChange={event => {
|
|
140
|
+
updateField(section, subsection, fieldName, event.target.value)
|
|
141
|
+
}}
|
|
142
|
+
{...attributes}
|
|
143
|
+
>
|
|
144
|
+
{optionsJsx}
|
|
145
|
+
</select>
|
|
146
|
+
</label>
|
|
147
|
+
)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
export { Select, CheckBox, TextField }
|
package/components/Filters.jsx
CHANGED
|
@@ -89,7 +89,7 @@ export const useFilters = props => {
|
|
|
89
89
|
filters: newFilters
|
|
90
90
|
})
|
|
91
91
|
}
|
|
92
|
-
|
|
92
|
+
|
|
93
93
|
// Used for setting active filter, fromHash breaks the filteredData functionality.
|
|
94
94
|
if (visualizationConfig.type === 'map' && visualizationConfig.filterBehavior === 'Filter Change') {
|
|
95
95
|
setFilteredData(newFilters)
|
|
@@ -320,7 +320,7 @@ const Filters = props => {
|
|
|
320
320
|
const tabValues = []
|
|
321
321
|
const tabBarValues = []
|
|
322
322
|
|
|
323
|
-
const { active, label, filterStyle } = singleFilter
|
|
323
|
+
const { active, queuedActive, label, filterStyle } = singleFilter
|
|
324
324
|
|
|
325
325
|
handleSorting(singleFilter)
|
|
326
326
|
|
|
@@ -387,7 +387,7 @@ const Filters = props => {
|
|
|
387
387
|
{filterStyle === 'tab' && !mobileFilterStyle && <Filters.Tabs tabs={tabValues} />}
|
|
388
388
|
{filterStyle === 'pill' && !mobileFilterStyle && <Filters.Pills pills={pillValues} />}
|
|
389
389
|
{filterStyle === 'tab bar' && !mobileFilterStyle && <Filters.TabBar filter={singleFilter} index={outerIndex} />}
|
|
390
|
-
{(filterStyle === 'dropdown' || mobileFilterStyle) && <Filters.Dropdown filter={singleFilter} index={outerIndex} label={label} active={active} filters={values} />}
|
|
390
|
+
{(filterStyle === 'dropdown' || mobileFilterStyle) && <Filters.Dropdown filter={singleFilter} index={outerIndex} label={label} active={queuedActive || active} filters={values} />}
|
|
391
391
|
</>
|
|
392
392
|
</div>
|
|
393
393
|
)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
|
|
3
|
-
export default function LegendCircle({ fill, borderColor }) {
|
|
3
|
+
export default function LegendCircle({ fill, borderColor, display = 'inline-block' }) {
|
|
4
4
|
const styles = {
|
|
5
5
|
marginRight: '5px',
|
|
6
6
|
borderRadius: '300px',
|
|
7
7
|
verticalAlign: 'middle',
|
|
8
|
-
display:
|
|
8
|
+
display: display,
|
|
9
9
|
height: '1em',
|
|
10
10
|
width: '1em',
|
|
11
11
|
border: borderColor ? `${borderColor} 1px solid` : 'rgba(0,0,0,.3) 1px solid',
|
|
@@ -59,7 +59,7 @@ const generateMedia = (state, type, elementToCapture) => {
|
|
|
59
59
|
|
|
60
60
|
switch (type) {
|
|
61
61
|
case 'image':
|
|
62
|
-
html2canvas(baseSvg, {foreignObjectRendering: true}).then(canvas => {
|
|
62
|
+
html2canvas(baseSvg, {foreignObjectRendering: true, x: -1 * (window.pageXOffset + baseSvg.getBoundingClientRect().left), y: -1 * (window.pageYOffset + baseSvg.getBoundingClientRect().top)}).then(canvas => {
|
|
63
63
|
saveImageAs(canvas.toDataURL(), filename + '.png')
|
|
64
64
|
})
|
|
65
65
|
return
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import Icon from '../ui/Icon'
|
|
3
|
+
|
|
4
|
+
import './multiselect.styles.css'
|
|
5
|
+
import { UpdateFieldFunc } from '../../types/UpdateFieldFunc'
|
|
6
|
+
|
|
7
|
+
interface Option {
|
|
8
|
+
value: string
|
|
9
|
+
label: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface MultiSelectProps {
|
|
13
|
+
section?: string
|
|
14
|
+
subsection?: string
|
|
15
|
+
fieldName: string
|
|
16
|
+
options: Option[]
|
|
17
|
+
updateField: UpdateFieldFunc<string[]>
|
|
18
|
+
label?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection = null, fieldName, label, options, updateField }) => {
|
|
22
|
+
const [selectedItems, setSelectedItems] = useState<Option[]>([])
|
|
23
|
+
const [expanded, setExpanded] = useState(false)
|
|
24
|
+
const multiSelectRef = useRef(null)
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const handleClickOutside = event => {
|
|
28
|
+
if (multiSelectRef.current && !multiSelectRef.current.contains(event.target)) {
|
|
29
|
+
setExpanded(false)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
34
|
+
|
|
35
|
+
return () => {
|
|
36
|
+
document.removeEventListener('mousedown', handleClickOutside)
|
|
37
|
+
}
|
|
38
|
+
}, [])
|
|
39
|
+
|
|
40
|
+
const update = newItems =>
|
|
41
|
+
updateField(
|
|
42
|
+
section,
|
|
43
|
+
subsection,
|
|
44
|
+
fieldName,
|
|
45
|
+
newItems.map(item => item.value)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const handleItemSelect = (option: Option) => {
|
|
49
|
+
const newItems = [...selectedItems, option]
|
|
50
|
+
setSelectedItems(newItems)
|
|
51
|
+
update(newItems)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const handleItemRemove = (option: Option, caller: string) => {
|
|
55
|
+
const newItems = selectedItems.filter(item => item.value !== option.value)
|
|
56
|
+
setSelectedItems(newItems)
|
|
57
|
+
update(newItems)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const multiID = 'multiSelect_' + label
|
|
61
|
+
return (
|
|
62
|
+
<div ref={multiSelectRef} className='cove-multiselect'>
|
|
63
|
+
{label && (
|
|
64
|
+
<span id={multiID} className='edit-label cove-input__label'>
|
|
65
|
+
{label}
|
|
66
|
+
</span>
|
|
67
|
+
)}
|
|
68
|
+
|
|
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, 'button click')} onKeyUp={() => handleItemRemove(item, 'button key up')}>
|
|
72
|
+
{item.label}
|
|
73
|
+
<button aria-label='Remove' onClick={() => handleItemRemove(item, 'X')}>
|
|
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>
|
|
81
|
+
</div>
|
|
82
|
+
<ul className={'dropdown' + (expanded ? '' : ' hide')}>
|
|
83
|
+
{options
|
|
84
|
+
.filter(option => !selectedItems.find(item => item.value === option.value))
|
|
85
|
+
.map(option => (
|
|
86
|
+
<li className='cove-multiselect-li' key={option.value} role='option' tabIndex={0} onClick={() => handleItemSelect(option)} onKeyUp={() => handleItemSelect(option)}>
|
|
87
|
+
{option.label}
|
|
88
|
+
</li>
|
|
89
|
+
))}
|
|
90
|
+
</ul>
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default MultiSelect
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './MultiSelect'
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
.cove-multiselect {
|
|
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) {
|
|
12
|
+
display: inline-block;
|
|
13
|
+
padding: 0 0 0 5px;
|
|
14
|
+
margin-right: 5px;
|
|
15
|
+
margin-bottom: 2px;
|
|
16
|
+
background: #ccc;
|
|
17
|
+
border-radius: 5px;
|
|
18
|
+
}
|
|
19
|
+
.expand {
|
|
20
|
+
padding: 0 5px;
|
|
21
|
+
border-radius: 5px;
|
|
22
|
+
background: #ccc;
|
|
23
|
+
float: right;
|
|
24
|
+
}
|
|
25
|
+
border-radius: 5px;
|
|
26
|
+
}
|
|
27
|
+
.dropdown {
|
|
28
|
+
background: white;
|
|
29
|
+
position: absolute;
|
|
30
|
+
margin-top: 5px;
|
|
31
|
+
border: 1px solid #ccc;
|
|
32
|
+
padding: 0;
|
|
33
|
+
min-height: 40px;
|
|
34
|
+
overflow: scroll;
|
|
35
|
+
max-height: 200px;
|
|
36
|
+
z-index: 1;
|
|
37
|
+
&.hide {
|
|
38
|
+
display: none;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
:is(li) {
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
list-style: none;
|
|
44
|
+
padding-left: 10px;
|
|
45
|
+
&:hover {
|
|
46
|
+
background: #ccc;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ReactNode } from 'react'
|
|
2
|
+
import Row from './components/Row'
|
|
3
|
+
import GroupRow from './components/GroupRow'
|
|
4
|
+
import { CellMatrix, GroupCellMatrix } from './types/CellMatrix'
|
|
5
|
+
import { RowType } from './types/RowType'
|
|
6
|
+
|
|
7
|
+
type TableProps = {
|
|
8
|
+
childrenMatrix: CellMatrix | GroupCellMatrix
|
|
9
|
+
tableName: string
|
|
10
|
+
caption: string
|
|
11
|
+
stickyHeader?: boolean
|
|
12
|
+
headContent: ReactNode
|
|
13
|
+
tableOptions: {
|
|
14
|
+
className: string
|
|
15
|
+
'aria-live'?: 'off' | 'assertive' | 'polite'
|
|
16
|
+
hidden?: boolean
|
|
17
|
+
'aria-rowcount'?: number
|
|
18
|
+
cellMinWidth?: number
|
|
19
|
+
}
|
|
20
|
+
wrapColumns?: boolean
|
|
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
|
+
}
|
|
23
|
+
|
|
24
|
+
type Position = 'sticky'
|
|
25
|
+
|
|
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 isGroupedMatrix = !Array.isArray(childrenMatrix)
|
|
29
|
+
return (
|
|
30
|
+
<table {...tableOptions}>
|
|
31
|
+
<caption className='visually-hidden'>{caption}</caption>
|
|
32
|
+
<thead style={headStyle}>{headContent}</thead>
|
|
33
|
+
<tbody>
|
|
34
|
+
{isGroupedMatrix
|
|
35
|
+
? Object.keys(childrenMatrix).flatMap(groupName => {
|
|
36
|
+
let colSpan = 0
|
|
37
|
+
const rows = childrenMatrix[groupName].map((row, i) => {
|
|
38
|
+
colSpan = row.length
|
|
39
|
+
const key = `${tableName}-${groupName}-row-${i}`
|
|
40
|
+
return <Row key={key} rowKey={key} childRow={row} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} />
|
|
41
|
+
})
|
|
42
|
+
return [<GroupRow label={groupName} colSpan={colSpan} key={`${tableName}-${groupName}`} />, ...rows]
|
|
43
|
+
})
|
|
44
|
+
: childrenMatrix.map((childRow, i) => {
|
|
45
|
+
let childRowCopy = [...childRow]
|
|
46
|
+
let rowType = undefined
|
|
47
|
+
if (hasRowType) rowType = childRowCopy.shift()
|
|
48
|
+
const key = `${tableName}-row-${i}`
|
|
49
|
+
if (rowType === undefined) {
|
|
50
|
+
return <Row key={key} rowKey={key} childRow={childRow} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} />
|
|
51
|
+
} else {
|
|
52
|
+
switch (rowType) {
|
|
53
|
+
case RowType.row_group:
|
|
54
|
+
return <GroupRow label={childRowCopy[0]} colSpan={childRowCopy.length} key={key} />
|
|
55
|
+
case RowType.total:
|
|
56
|
+
return <Row key={key} rowKey={key} childRow={childRowCopy} isTotal={true} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} />
|
|
57
|
+
case RowType.row_group_total:
|
|
58
|
+
return <GroupRow label={childRowCopy[0]} colSpan={1} key={key} data={childRowCopy.slice(1)} />
|
|
59
|
+
default:
|
|
60
|
+
return <Row key={key} rowKey={key} childRow={childRowCopy} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} />
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
})}
|
|
64
|
+
</tbody>
|
|
65
|
+
</table>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default Table
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
type GroupRowProps = {
|
|
4
|
+
label: ReactNode
|
|
5
|
+
colSpan: number
|
|
6
|
+
data?: ReactNode[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const GroupRow = ({ label, colSpan, data }: GroupRowProps) => {
|
|
10
|
+
return (
|
|
11
|
+
<tr>
|
|
12
|
+
<th scope='colgroup' colSpan={colSpan}>
|
|
13
|
+
{label}
|
|
14
|
+
</th>
|
|
15
|
+
{data && data.map((item, i) => <th key={`${label}-${i}`}>{item}</th>)}
|
|
16
|
+
</tr>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default GroupRow
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ReactNode } from 'react'
|
|
2
|
+
import Cell from './Cell'
|
|
3
|
+
|
|
4
|
+
type RowProps = {
|
|
5
|
+
childRow: ReactNode[]
|
|
6
|
+
rowKey: string
|
|
7
|
+
wrapColumns: boolean
|
|
8
|
+
isTotal?: boolean
|
|
9
|
+
cellMinWidth?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const Row = ({ childRow, rowKey, wrapColumns, cellMinWidth = 0, isTotal }: RowProps) => {
|
|
13
|
+
const whiteSpace = wrapColumns ? 'unset' : 'nowrap'
|
|
14
|
+
const minWidth = cellMinWidth + 'px'
|
|
15
|
+
return (
|
|
16
|
+
<tr>
|
|
17
|
+
{childRow.map((child, i) => (
|
|
18
|
+
<Cell key={rowKey + '__' + i} style={{ whiteSpace, minWidth }} isBold={isTotal}>
|
|
19
|
+
{child}
|
|
20
|
+
</Cell>
|
|
21
|
+
))}
|
|
22
|
+
</tr>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default Row
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './Table'
|