@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.
Files changed (74) hide show
  1. package/LICENSE +201 -0
  2. package/assets/icon-deviation-bar.svg +1 -0
  3. package/components/DataTable/DataTable.tsx +223 -0
  4. package/components/DataTable/components/BoxplotHeader.tsx +16 -0
  5. package/components/DataTable/components/CellAnchor.tsx +44 -0
  6. package/components/DataTable/components/ChartHeader.tsx +103 -0
  7. package/components/DataTable/components/ExpandCollapse.tsx +21 -0
  8. package/components/DataTable/components/Icons.tsx +10 -0
  9. package/components/DataTable/components/MapHeader.tsx +56 -0
  10. package/components/DataTable/components/SkipNav.tsx +7 -0
  11. package/components/DataTable/helpers/boxplotCellMatrix.tsx +64 -0
  12. package/components/DataTable/helpers/chartCellMatrix.tsx +92 -0
  13. package/components/DataTable/helpers/customColumns.ts +25 -0
  14. package/components/DataTable/helpers/customSort.ts +55 -0
  15. package/components/DataTable/helpers/getChartCellValue.ts +56 -0
  16. package/components/DataTable/helpers/getDataSeriesColumns.ts +29 -0
  17. package/components/DataTable/helpers/getSeriesName.ts +26 -0
  18. package/components/DataTable/helpers/mapCellMatrix.tsx +56 -0
  19. package/components/DataTable/helpers/regionCellMatrix.tsx +13 -0
  20. package/components/DataTable/helpers/standardizeState.js +76 -0
  21. package/components/DataTable/index.ts +1 -0
  22. package/components/DataTable/types/TableConfig.ts +52 -0
  23. package/components/DownloadButton.tsx +29 -0
  24. package/components/EditorPanel/DataTableEditor.tsx +133 -0
  25. package/components/EditorPanel/Inputs.tsx +150 -0
  26. package/components/Filters.jsx +3 -3
  27. package/components/LegendCircle.jsx +2 -2
  28. package/components/MediaControls.jsx +1 -1
  29. package/components/MultiSelect/MultiSelect.tsx +95 -0
  30. package/components/MultiSelect/index.ts +1 -0
  31. package/components/MultiSelect/multiselect.styles.css +50 -0
  32. package/components/Table/Table.tsx +69 -0
  33. package/components/Table/components/Cell.tsx +9 -0
  34. package/components/Table/components/GroupRow.tsx +20 -0
  35. package/components/Table/components/Row.tsx +26 -0
  36. package/components/Table/index.ts +1 -0
  37. package/components/Table/types/CellMatrix.ts +4 -0
  38. package/components/Table/types/RowType.ts +5 -0
  39. package/components/_stories/DataTable.stories.tsx +103 -0
  40. package/components/_stories/EditorPanel.stories.tsx +53 -0
  41. package/components/_stories/Inputs.stories.tsx +37 -0
  42. package/components/_stories/MultiSelect.stories.tsx +24 -0
  43. package/components/_stories/Table.stories.tsx +53 -0
  44. package/components/_stories/_mocks/dashboard_no_filter.json +121 -0
  45. package/components/_stories/_mocks/example-city-state.json +808 -0
  46. package/components/_stories/_mocks/row_type.json +42 -0
  47. package/components/_stories/styles.scss +9 -0
  48. package/components/inputs/{InputSelect.jsx → InputSelect.tsx} +15 -5
  49. package/components/managers/{DataDesigner.jsx → DataDesigner.tsx} +103 -94
  50. package/components/ui/{Icon.jsx → Icon.tsx} +3 -3
  51. package/components/ui/Title/Title.scss +95 -0
  52. package/components/ui/Title/index.tsx +34 -0
  53. package/components/ui/_stories/Title.stories.tsx +21 -0
  54. package/helpers/DataTransform.ts +75 -20
  55. package/helpers/cove/string.ts +11 -0
  56. package/helpers/fetchRemoteData.js +1 -1
  57. package/helpers/getFileExtension.ts +28 -5
  58. package/package.json +2 -2
  59. package/styles/_data-table.scss +3 -0
  60. package/styles/heading-colors.scss +0 -3
  61. package/styles/v2/layout/_component.scss +0 -11
  62. package/types/Axis.ts +41 -0
  63. package/types/Color.ts +5 -0
  64. package/types/Column.ts +15 -0
  65. package/types/ComponentStyles.ts +7 -0
  66. package/types/ComponentThemes.ts +13 -0
  67. package/types/EditorColumnProperties.ts +8 -0
  68. package/types/FilterBehavior.ts +1 -0
  69. package/types/Runtime.ts +29 -0
  70. package/types/Series.ts +1 -0
  71. package/types/Table.ts +18 -0
  72. package/types/UpdateFieldFunc.ts +1 -0
  73. package/types/Visualization.ts +21 -0
  74. 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 }
@@ -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: 'inline-block',
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,9 @@
1
+ const Cell = ({ children, style, isBold = false }) => {
2
+ return (
3
+ <td tabIndex={0} role='gridcell' style={style}>
4
+ {isBold ? <strong>{children}</strong> : children}
5
+ </td>
6
+ )
7
+ }
8
+
9
+ export default Cell
@@ -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'
@@ -0,0 +1,4 @@
1
+ import { ReactNode } from 'react'
2
+
3
+ export type CellMatrix = ReactNode[][]
4
+ export type GroupCellMatrix = Record<string, CellMatrix>
@@ -0,0 +1,5 @@
1
+ export enum RowType {
2
+ row_group = 'row_group',
3
+ total = 'total',
4
+ row_group_total = 'row_group_total'
5
+ }