@cdc/core 4.25.1 → 4.25.2-25
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/components/DataTable/DataTable.tsx +32 -9
- package/components/DataTable/components/ChartHeader.tsx +25 -6
- package/components/DataTable/components/MapHeader.tsx +5 -1
- package/components/DataTable/data-table.css +1 -1
- package/components/DataTable/helpers/chartCellMatrix.tsx +14 -5
- package/components/EditorPanel/Inputs.tsx +4 -0
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +1 -1
- package/components/Filters/Filters.tsx +55 -17
- package/components/Filters/helpers/filterWrapping.ts +43 -0
- package/components/Filters/helpers/handleSorting.ts +1 -0
- package/components/Filters/helpers/tests/handleSorting.test.ts +26 -0
- package/components/NestedDropdown/NestedDropdown.tsx +18 -5
- package/components/NestedDropdown/nesteddropdown.styles.css +2 -2
- package/components/Table/Table.tsx +11 -5
- package/components/Table/components/Row.tsx +14 -2
- package/dist/cove-main.css +183 -132
- package/dist/cove-main.css.map +1 -1
- package/helpers/DataTransform.ts +11 -9
- package/helpers/formatConfigBeforeSave.ts +1 -1
- package/helpers/isRightAlignedTableValue.js +14 -0
- package/helpers/queryStringUtils.ts +7 -0
- package/helpers/ver/4.24.7.ts +19 -1
- package/package.json +2 -2
- package/styles/filters.scss +60 -0
- package/styles/v2/themes/_color-definitions.scss +4 -4
- package/types/VizFilter.ts +1 -0
|
@@ -21,6 +21,7 @@ import { TableConfig } from './types/TableConfig'
|
|
|
21
21
|
import { Column } from '../../types/Column'
|
|
22
22
|
import { pivotData } from '../../helpers/pivotData'
|
|
23
23
|
import { isLegendWrapViewport } from '@cdc/core/helpers/viewports'
|
|
24
|
+
import isRightAlignedTableValue from '@cdc/core/helpers/isRightAlignedTableValue'
|
|
24
25
|
import './data-table.css'
|
|
25
26
|
import _ from 'lodash'
|
|
26
27
|
|
|
@@ -171,7 +172,7 @@ const DataTable = (props: DataTableProps) => {
|
|
|
171
172
|
if (config.type === 'map') {
|
|
172
173
|
return config.table.caption
|
|
173
174
|
? config.table.caption
|
|
174
|
-
: `Data table showing data for the ${mapLookup[config.general
|
|
175
|
+
: `Data table showing data for the ${mapLookup[config.general?.geoType]} figure.`
|
|
175
176
|
} else {
|
|
176
177
|
return config.table.caption ? config.table.caption : `Data table showing data for the ${config.type} figure.`
|
|
177
178
|
}
|
|
@@ -249,6 +250,22 @@ const DataTable = (props: DataTableProps) => {
|
|
|
249
250
|
return classes
|
|
250
251
|
}
|
|
251
252
|
|
|
253
|
+
const childrenMatrix =
|
|
254
|
+
config.type === 'map'
|
|
255
|
+
? mapCellMatrix({ rows, wrapColumns, ...props, runtimeData, viewport })
|
|
256
|
+
: chartCellMatrix({ rows, ...props, runtimeData, isVertical, sortBy, hasRowType, viewport })
|
|
257
|
+
|
|
258
|
+
// If every value in a column is a number, record the column index so the header and cells can be right-aligned
|
|
259
|
+
const rightAlignedCols = childrenMatrix.length
|
|
260
|
+
? Object.fromEntries(
|
|
261
|
+
Object.keys(childrenMatrix[0])
|
|
262
|
+
.filter(
|
|
263
|
+
i => childrenMatrix.filter(row => isRightAlignedTableValue(row[i])).length === childrenMatrix.length
|
|
264
|
+
)
|
|
265
|
+
.map(x => [x, true])
|
|
266
|
+
)
|
|
267
|
+
: {}
|
|
268
|
+
|
|
252
269
|
const TableMediaControls = ({ belowTable }) => {
|
|
253
270
|
return (
|
|
254
271
|
<MediaControls.Section classes={getMediaControlsClasses(belowTable)}>
|
|
@@ -266,7 +283,7 @@ const DataTable = (props: DataTableProps) => {
|
|
|
266
283
|
|
|
267
284
|
return (
|
|
268
285
|
<ErrorBoundary component='DataTable'>
|
|
269
|
-
{!config.table.showDownloadLinkBelow && <TableMediaControls />}
|
|
286
|
+
{config.general?.showDownloadButton && !config.table.showDownloadLinkBelow && <TableMediaControls />}
|
|
270
287
|
<section id={tabbingId.replace('#', '')} className={getClassNames()} aria-label={accessibilityLabel}>
|
|
271
288
|
<SkipTo skipId={skipId} skipMessage='Skip Data Table' />
|
|
272
289
|
{config.table.collapsible !== false && (
|
|
@@ -278,18 +295,20 @@ const DataTable = (props: DataTableProps) => {
|
|
|
278
295
|
viewport={viewport}
|
|
279
296
|
wrapColumns={wrapColumns}
|
|
280
297
|
noData={hasNoData}
|
|
281
|
-
childrenMatrix={
|
|
282
|
-
config.type === 'map'
|
|
283
|
-
? mapCellMatrix({ rows, wrapColumns, ...props, runtimeData, viewport })
|
|
284
|
-
: chartCellMatrix({ rows, ...props, runtimeData, isVertical, sortBy, hasRowType, viewport })
|
|
285
|
-
}
|
|
298
|
+
childrenMatrix={childrenMatrix}
|
|
286
299
|
tableName={config.type}
|
|
287
300
|
caption={caption}
|
|
288
301
|
stickyHeader
|
|
289
302
|
hasRowType={hasRowType}
|
|
290
303
|
headContent={
|
|
291
304
|
config.type === 'map' ? (
|
|
292
|
-
<MapHeader
|
|
305
|
+
<MapHeader
|
|
306
|
+
columns={columns}
|
|
307
|
+
{...props}
|
|
308
|
+
sortBy={sortBy}
|
|
309
|
+
setSortBy={setSortBy}
|
|
310
|
+
rightAlignedCols={rightAlignedCols}
|
|
311
|
+
/>
|
|
293
312
|
) : (
|
|
294
313
|
<ChartHeader
|
|
295
314
|
data={runtimeData}
|
|
@@ -299,6 +318,7 @@ const DataTable = (props: DataTableProps) => {
|
|
|
299
318
|
sortBy={sortBy}
|
|
300
319
|
setSortBy={setSortBy}
|
|
301
320
|
viewport={viewport}
|
|
321
|
+
rightAlignedCols={rightAlignedCols}
|
|
302
322
|
/>
|
|
303
323
|
)
|
|
304
324
|
}
|
|
@@ -310,6 +330,7 @@ const DataTable = (props: DataTableProps) => {
|
|
|
310
330
|
'aria-rowcount': config?.data?.length ? config.data.length : -1,
|
|
311
331
|
hidden: !expanded
|
|
312
332
|
}}
|
|
333
|
+
rightAlignedCols={rightAlignedCols}
|
|
313
334
|
/>
|
|
314
335
|
|
|
315
336
|
{/* REGION Data Table */}
|
|
@@ -336,7 +357,9 @@ const DataTable = (props: DataTableProps) => {
|
|
|
336
357
|
)}
|
|
337
358
|
</div>
|
|
338
359
|
</section>
|
|
339
|
-
{config.table.showDownloadLinkBelow &&
|
|
360
|
+
{config.general?.showDownloadButton && config.table.showDownloadLinkBelow && (
|
|
361
|
+
<TableMediaControls belowTable={true} />
|
|
362
|
+
)}
|
|
340
363
|
<div id={skipId} className='cdcdataviz-sr-only'>
|
|
341
364
|
Skipped data table.
|
|
342
365
|
</div>
|
|
@@ -5,9 +5,18 @@ import ScreenReaderText from '@cdc/core/components/elements/ScreenReaderText'
|
|
|
5
5
|
import { SortIcon } from './SortIcon'
|
|
6
6
|
import { getNewSortBy } from '../helpers/getNewSortBy'
|
|
7
7
|
|
|
8
|
-
type ChartHeaderProps = { data; isVertical; config; setSortBy; sortBy; hasRowType?; viewport }
|
|
8
|
+
type ChartHeaderProps = { data; isVertical; config; setSortBy; sortBy; hasRowType?; viewport; rightAlignedCols }
|
|
9
9
|
|
|
10
|
-
const ChartHeader = ({
|
|
10
|
+
const ChartHeader = ({
|
|
11
|
+
data,
|
|
12
|
+
isVertical,
|
|
13
|
+
config,
|
|
14
|
+
setSortBy,
|
|
15
|
+
sortBy,
|
|
16
|
+
hasRowType,
|
|
17
|
+
viewport,
|
|
18
|
+
rightAlignedCols
|
|
19
|
+
}: ChartHeaderProps) => {
|
|
11
20
|
const groupBy = config.table?.groupBy
|
|
12
21
|
if (!data) return
|
|
13
22
|
let dataSeriesColumns = getDataSeriesColumns(config, isVertical, data)
|
|
@@ -69,10 +78,15 @@ const ChartHeader = ({ data, isVertical, config, setSortBy, sortBy, hasRowType,
|
|
|
69
78
|
const text = getSeriesName(column, config)
|
|
70
79
|
const newSortBy = getNewSortBy(sortBy, column, index)
|
|
71
80
|
const sortByAsc = sortBy.column === column ? sortBy.asc : undefined
|
|
81
|
+
const isSortedCol = column === sortBy.column && !hasRowType
|
|
72
82
|
|
|
73
83
|
return (
|
|
74
84
|
<th
|
|
75
|
-
style={{
|
|
85
|
+
style={{
|
|
86
|
+
minWidth: (config.table.cellMinWidth || 0) + 'px',
|
|
87
|
+
textAlign: rightAlignedCols && rightAlignedCols[index] ? 'right' : '',
|
|
88
|
+
paddingRight: isSortedCol ? '1.3em' : ''
|
|
89
|
+
}}
|
|
76
90
|
key={`col-header-${column}__${index}`}
|
|
77
91
|
tabIndex={0}
|
|
78
92
|
role='columnheader'
|
|
@@ -94,7 +108,7 @@ const ChartHeader = ({ data, isVertical, config, setSortBy, sortBy, hasRowType,
|
|
|
94
108
|
: null)}
|
|
95
109
|
>
|
|
96
110
|
<ColumnHeadingText text={text} column={column} config={config} />
|
|
97
|
-
{
|
|
111
|
+
{isSortedCol && <SortIcon ascending={sortByAsc} />}
|
|
98
112
|
<ScreenReaderSortByText sortBy={sortBy} config={config} text={text} />
|
|
99
113
|
</th>
|
|
100
114
|
)
|
|
@@ -110,9 +124,14 @@ const ChartHeader = ({ data, isVertical, config, setSortBy, sortBy, hasRowType,
|
|
|
110
124
|
let text = row !== '__series__' ? getChartCellValue(row, column, config, data) : '__series__'
|
|
111
125
|
const newSortBy = getNewSortBy(sortBy, column, index)
|
|
112
126
|
const sortByAsc = sortBy.colIndex === index ? sortBy.asc : undefined
|
|
127
|
+
const isSortedCol = index === sortBy.colIndex && !hasRowType
|
|
113
128
|
return (
|
|
114
129
|
<th
|
|
115
|
-
style={{
|
|
130
|
+
style={{
|
|
131
|
+
minWidth: (config.table.cellMinWidth || 0) + 'px',
|
|
132
|
+
textAlign: rightAlignedCols && rightAlignedCols[index] ? 'right' : '',
|
|
133
|
+
paddingRight: isSortedCol ? '1.3em' : ''
|
|
134
|
+
}}
|
|
116
135
|
key={`col-header-${text}__${index}`}
|
|
117
136
|
tabIndex={0}
|
|
118
137
|
role='columnheader'
|
|
@@ -132,7 +151,7 @@ const ChartHeader = ({ data, isVertical, config, setSortBy, sortBy, hasRowType,
|
|
|
132
151
|
: null)}
|
|
133
152
|
>
|
|
134
153
|
<ColumnHeadingText text={text} column={column} config={config} />
|
|
135
|
-
{
|
|
154
|
+
{isSortedCol && <SortIcon ascending={sortByAsc} />}
|
|
136
155
|
|
|
137
156
|
<ScreenReaderSortByText text={text} config={config} sortBy={sortBy} />
|
|
138
157
|
</th>
|
|
@@ -16,7 +16,7 @@ const ColumnHeadingText = ({ text, config }) => {
|
|
|
16
16
|
return text
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const MapHeader = ({ columns, config, indexTitle, sortBy, setSortBy }: MapHeaderProps) => {
|
|
19
|
+
const MapHeader = ({ columns, config, indexTitle, sortBy, setSortBy, rightAlignedCols }: MapHeaderProps) => {
|
|
20
20
|
return (
|
|
21
21
|
<tr>
|
|
22
22
|
{Object.keys(columns)
|
|
@@ -35,6 +35,10 @@ const MapHeader = ({ columns, config, indexTitle, sortBy, setSortBy }: MapHeader
|
|
|
35
35
|
const sortByAsc = sortBy.column === column ? sortBy.asc : undefined
|
|
36
36
|
return (
|
|
37
37
|
<th
|
|
38
|
+
style={{
|
|
39
|
+
textAlign: rightAlignedCols && rightAlignedCols[index] ? 'right' : '',
|
|
40
|
+
paddingRight: '1.3em'
|
|
41
|
+
}}
|
|
38
42
|
key={`col-header-${column}__${index}`}
|
|
39
43
|
id={column}
|
|
40
44
|
tabIndex={0}
|
|
@@ -16,7 +16,15 @@ type ChartRowsProps = DataTableProps & {
|
|
|
16
16
|
hasRowType?: boolean
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const chartCellArray = ({
|
|
19
|
+
const chartCellArray = ({
|
|
20
|
+
rows,
|
|
21
|
+
runtimeData,
|
|
22
|
+
config,
|
|
23
|
+
isVertical,
|
|
24
|
+
sortBy,
|
|
25
|
+
colorScale,
|
|
26
|
+
hasRowType
|
|
27
|
+
}: ChartRowsProps): CellMatrix | GroupCellMatrix => {
|
|
20
28
|
const groupBy = config.table?.groupBy
|
|
21
29
|
const dataSeriesColumns = getDataSeriesColumns(config, isVertical, runtimeData)
|
|
22
30
|
|
|
@@ -38,7 +46,7 @@ const chartCellArray = ({ rows, runtimeData, config, isVertical, sortBy, colorSc
|
|
|
38
46
|
|
|
39
47
|
if (isVertical) {
|
|
40
48
|
if (groupBy) {
|
|
41
|
-
const cellMatrix
|
|
49
|
+
const cellMatrix = new Map()
|
|
42
50
|
rows.forEach(row => {
|
|
43
51
|
let groupKey: string
|
|
44
52
|
let groupValues = []
|
|
@@ -49,10 +57,11 @@ const chartCellArray = ({ rows, runtimeData, config, isVertical, sortBy, colorSc
|
|
|
49
57
|
groupValues.push(getChartCellValue(row, column, config, runtimeData))
|
|
50
58
|
}
|
|
51
59
|
})
|
|
52
|
-
if (!cellMatrix
|
|
53
|
-
cellMatrix
|
|
60
|
+
if (!cellMatrix.has(groupKey)) {
|
|
61
|
+
cellMatrix.set(groupKey, [groupValues])
|
|
54
62
|
} else {
|
|
55
|
-
cellMatrix
|
|
63
|
+
const currentGroupValues = cellMatrix.get(groupKey)
|
|
64
|
+
cellMatrix.set(groupKey, [...currentGroupValues, groupValues])
|
|
56
65
|
}
|
|
57
66
|
})
|
|
58
67
|
return cellMatrix
|
|
@@ -55,6 +55,10 @@ const TextField = memo((props: TextFieldProps) => {
|
|
|
55
55
|
}
|
|
56
56
|
}, [debouncedValue])
|
|
57
57
|
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
setValue(stateValue) // Update local state when props change
|
|
60
|
+
}, [stateValue])
|
|
61
|
+
|
|
58
62
|
let name = subsection ? `${section}-${subsection}-${fieldName}` : `${section}-${subsection}-${fieldName}`
|
|
59
63
|
|
|
60
64
|
const onChange = e => {
|
|
@@ -91,7 +91,7 @@ const VizFilterEditor: React.FC<VizFilterProps> = ({ config, updateField, rawDat
|
|
|
91
91
|
// Overwrite filterItem.values since thats what we map through in the editor panel
|
|
92
92
|
filterItem.values = updatedValues
|
|
93
93
|
filterItem.orderedValues = updatedValues
|
|
94
|
-
filterItem.active = updatedValues[0]
|
|
94
|
+
filterItem.active = filterItem.defaultValue ? filterItem.defaultValue : updatedValues[0]
|
|
95
95
|
|
|
96
96
|
filterItem.order = 'cust'
|
|
97
97
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useState, useEffect, useMemo } from 'react'
|
|
2
|
-
import
|
|
1
|
+
import { useState, useEffect, useMemo, useRef, useId } from 'react'
|
|
2
|
+
import _ from 'lodash'
|
|
3
3
|
|
|
4
4
|
// CDC
|
|
5
5
|
import Button from '../elements/Button'
|
|
@@ -11,16 +11,17 @@ import { filterVizData } from '../../helpers/filterVizData'
|
|
|
11
11
|
import { addValuesToFilters } from '../../helpers/addValuesToFilters'
|
|
12
12
|
import { DimensionsType } from '../../types/Dimensions'
|
|
13
13
|
import NestedDropdown from '../NestedDropdown'
|
|
14
|
-
import _ from 'lodash'
|
|
15
14
|
import { getNestedOptions } from './helpers/getNestedOptions'
|
|
16
15
|
import { applyQueuedActive } from './helpers/applyQueuedActive'
|
|
17
16
|
import { handleSorting } from './helpers/handleSorting'
|
|
17
|
+
import { getWrappingStatuses } from './helpers/filterWrapping'
|
|
18
18
|
|
|
19
19
|
export const VIZ_FILTER_STYLE = {
|
|
20
20
|
dropdown: 'dropdown',
|
|
21
21
|
nestedDropdown: 'nested-dropdown',
|
|
22
22
|
pill: 'pill',
|
|
23
23
|
tab: 'tab',
|
|
24
|
+
tabSimple: 'tab-simple',
|
|
24
25
|
tabBar: 'tab bar',
|
|
25
26
|
multiSelect: 'multi-select'
|
|
26
27
|
} as const
|
|
@@ -85,7 +86,7 @@ export const useFilters = props => {
|
|
|
85
86
|
// Overwrite filterItem.values since thats what we map through in the editor panel
|
|
86
87
|
filterItem.values = updatedValues
|
|
87
88
|
filterItem.orderedValues = updatedValues
|
|
88
|
-
filterItem.active = updatedValues[0]
|
|
89
|
+
if (!filterItem.active) filterItem.active = filterItem.defaultValue ? filterItem.defaultValue : updatedValues[0]
|
|
89
90
|
filterItem.order = 'cust'
|
|
90
91
|
|
|
91
92
|
// Update the filters
|
|
@@ -226,6 +227,7 @@ export const useFilters = props => {
|
|
|
226
227
|
if (!filter.values || filter.values.length === 0) {
|
|
227
228
|
filter.values = getUniqueValues(data, filter.columnName)
|
|
228
229
|
}
|
|
230
|
+
|
|
229
231
|
newFilters[i].active = handleSorting(filter).values[0]
|
|
230
232
|
|
|
231
233
|
if (filter.setByQueryParameter && queryParams[filter.setByQueryParameter] !== filter.active) {
|
|
@@ -282,8 +284,17 @@ const Filters = (props: FilterProps) => {
|
|
|
282
284
|
const { filters, general, theme, filterBehavior } = visualizationConfig
|
|
283
285
|
const [mobileFilterStyle, setMobileFilterStyle] = useState(false)
|
|
284
286
|
const [selectedFilter, setSelectedFilter] = useState<EventTarget>(null)
|
|
287
|
+
const [wrappingFilters, setWrappingFilters] = useState({})
|
|
285
288
|
const id = useId()
|
|
286
289
|
|
|
290
|
+
const wrappingFilterRefs = useRef({})
|
|
291
|
+
const filterWrappingStatusesToUpdate = getWrappingStatuses(wrappingFilterRefs, wrappingFilters, filters)
|
|
292
|
+
|
|
293
|
+
if (filterWrappingStatusesToUpdate.length) {
|
|
294
|
+
const validStatuses = filterWrappingStatusesToUpdate.filter(Boolean) as [string, any][]
|
|
295
|
+
setWrappingFilters({ ...wrappingFilters, ...Object.fromEntries(validStatuses) })
|
|
296
|
+
}
|
|
297
|
+
|
|
287
298
|
// useFilters hook provides data and logic for handling various filter functions
|
|
288
299
|
// prettier-ignore
|
|
289
300
|
const {
|
|
@@ -297,13 +308,32 @@ const Filters = (props: FilterProps) => {
|
|
|
297
308
|
|
|
298
309
|
useEffect(() => {
|
|
299
310
|
if (!dimensions) return
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
311
|
+
const [width] = dimensions
|
|
312
|
+
|
|
313
|
+
const isMobile = Number(width) < 768
|
|
314
|
+
const isTabSimple = filters?.some(filter => filter.filterStyle === VIZ_FILTER_STYLE.tabSimple)
|
|
315
|
+
|
|
316
|
+
const defaultToMobile = isMobile && filters?.length && !isTabSimple
|
|
317
|
+
|
|
318
|
+
setMobileFilterStyle(defaultToMobile)
|
|
305
319
|
}, [dimensions])
|
|
306
320
|
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
const noLongerTabSimple = Object.keys(wrappingFilters).filter(columnName => {
|
|
323
|
+
const filter = filters.find(filter => filter.columnName === columnName)
|
|
324
|
+
if (!filter) return false
|
|
325
|
+
return filter.filterStyle !== VIZ_FILTER_STYLE.tabSimple
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
if (!noLongerTabSimple.length) return
|
|
329
|
+
|
|
330
|
+
setWrappingFilters(
|
|
331
|
+
Object.fromEntries(
|
|
332
|
+
Object.entries(wrappingFilters).filter(([columnName]) => !noLongerTabSimple.includes(columnName))
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
}, [filters])
|
|
336
|
+
|
|
307
337
|
useEffect(() => {
|
|
308
338
|
if (selectedFilter) {
|
|
309
339
|
const el = document.getElementById(selectedFilter.id)
|
|
@@ -376,13 +406,17 @@ const Filters = (props: FilterProps) => {
|
|
|
376
406
|
const DropdownOptions = []
|
|
377
407
|
const Pills = []
|
|
378
408
|
const Tabs = []
|
|
409
|
+
const isTabSimple = singleFilter.filterStyle === 'tab-simple'
|
|
379
410
|
|
|
380
|
-
const { active, queuedActive, label, filterStyle } = singleFilter as VizFilter
|
|
411
|
+
const { active, queuedActive, label, filterStyle, columnName } = singleFilter as VizFilter
|
|
412
|
+
const { isDropdown } = wrappingFilters[columnName] || {}
|
|
381
413
|
|
|
382
414
|
handleSorting(singleFilter)
|
|
383
415
|
singleFilter.values?.forEach((filterOption, index) => {
|
|
384
|
-
const
|
|
385
|
-
|
|
416
|
+
const isActive = active === filterOption
|
|
417
|
+
|
|
418
|
+
const pillClassList = ['pill', isActive ? 'pill--active' : null, theme && theme]
|
|
419
|
+
const tabClassList = ['tab', isActive && 'tab--active', theme && theme, isTabSimple && 'tab--simple']
|
|
386
420
|
|
|
387
421
|
Pills.push(
|
|
388
422
|
<div className='pill__wrapper' key={`pill-${index}`}>
|
|
@@ -439,8 +473,8 @@ const Filters = (props: FilterProps) => {
|
|
|
439
473
|
'form-group',
|
|
440
474
|
mobileFilterStyle ? 'single-filters--dropdown' : `single-filters--${filterStyle}`
|
|
441
475
|
]
|
|
442
|
-
const mobileExempt = ['nested-dropdown', 'multi-select'].includes(filterStyle)
|
|
443
|
-
const showDefaultDropdown = (filterStyle === 'dropdown' || mobileFilterStyle) && !mobileExempt
|
|
476
|
+
const mobileExempt = ['nested-dropdown', 'multi-select', VIZ_FILTER_STYLE.tabSimple].includes(filterStyle)
|
|
477
|
+
const showDefaultDropdown = ((filterStyle === 'dropdown' || mobileFilterStyle) && !mobileExempt) || isDropdown
|
|
444
478
|
const [nestedActiveGroup, nestedActiveSubGroup] = useMemo<string[]>(() => {
|
|
445
479
|
if (filterStyle !== 'nested-dropdown') return []
|
|
446
480
|
return (singleFilter.queuedActive || [singleFilter.active, singleFilter.subGrouping?.active]) as [
|
|
@@ -448,15 +482,19 @@ const Filters = (props: FilterProps) => {
|
|
|
448
482
|
string
|
|
449
483
|
]
|
|
450
484
|
}, [singleFilter])
|
|
485
|
+
const hideLabelMargin = isTabSimple && !showDefaultDropdown
|
|
451
486
|
return (
|
|
452
|
-
<div className={classList.join(' ')} key={outerIndex}>
|
|
487
|
+
<div className={classList.join(' ')} key={outerIndex} ref={el => (wrappingFilterRefs.current[columnName] = el)}>
|
|
453
488
|
<>
|
|
454
489
|
{label && (
|
|
455
|
-
<label className=
|
|
490
|
+
<label className={`font-weight-bold mb-${hideLabelMargin ? '0' : '2'}`} htmlFor={`filter-${outerIndex}`}>
|
|
456
491
|
{label}
|
|
457
492
|
</label>
|
|
458
493
|
)}
|
|
459
494
|
{filterStyle === 'tab' && !mobileFilterStyle && Tabs}
|
|
495
|
+
{filterStyle === 'tab-simple' && !showDefaultDropdown && (
|
|
496
|
+
<div className='tab-simple-container d-flex w-100'>{Tabs}</div>
|
|
497
|
+
)}
|
|
460
498
|
{filterStyle === 'pill' && !mobileFilterStyle && Pills}
|
|
461
499
|
{filterStyle === 'tab bar' && !mobileFilterStyle && <TabBar filter={singleFilter} index={outerIndex} />}
|
|
462
500
|
{filterStyle === 'multi-select' && (
|
|
@@ -509,7 +547,7 @@ const Filters = (props: FilterProps) => {
|
|
|
509
547
|
{visualizationConfig.filterIntro && (
|
|
510
548
|
<p className='filters-section__intro-text mb-3'>{visualizationConfig.filterIntro}</p>
|
|
511
549
|
)}
|
|
512
|
-
<div className='d-flex flex-wrap w-100 mb-4 pb-2 filters-section__wrapper'>
|
|
550
|
+
<div className='d-flex flex-wrap w-100 mb-4 pb-2 filters-section__wrapper align-items-end'>
|
|
513
551
|
{' '}
|
|
514
552
|
<>
|
|
515
553
|
<Style />
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { VIZ_FILTER_STYLE } from '../Filters'
|
|
2
|
+
|
|
3
|
+
const WRAPPING_HEIGHT_THRESHOLD_NO_LABEL = 60
|
|
4
|
+
const WRAPPING_HEIGHT_THRESHOLD_WITH_LABEL = 100
|
|
5
|
+
export const getWrappingStatuses = (wrappingFilterRefs, wrappingFilters, allFilters) =>
|
|
6
|
+
Object.entries(wrappingFilterRefs.current)
|
|
7
|
+
.map(([columnValue, ref]) => {
|
|
8
|
+
if (!ref) return false
|
|
9
|
+
|
|
10
|
+
const filter = allFilters.find(filter => filter.columnName === columnValue)
|
|
11
|
+
const { filterStyle, label } = filter || {}
|
|
12
|
+
|
|
13
|
+
if (!filterStyle || filterStyle !== VIZ_FILTER_STYLE.tabSimple) return false
|
|
14
|
+
|
|
15
|
+
const filterStyleClass = (ref as HTMLElement).className
|
|
16
|
+
.split(' ')
|
|
17
|
+
.find(className => className.includes(filterStyle))
|
|
18
|
+
?.split('single-filters--')[1]
|
|
19
|
+
|
|
20
|
+
const classMatchesStyle = filterStyleClass && filterStyleClass === filterStyle
|
|
21
|
+
|
|
22
|
+
if (!classMatchesStyle) return false
|
|
23
|
+
|
|
24
|
+
const wrappingState = wrappingFilters[columnValue]
|
|
25
|
+
const { height, width } = (ref as HTMLElement).getBoundingClientRect()
|
|
26
|
+
const wrappingThreshold = label ? WRAPPING_HEIGHT_THRESHOLD_WITH_LABEL : WRAPPING_HEIGHT_THRESHOLD_NO_LABEL
|
|
27
|
+
const isWrapped = height > wrappingThreshold
|
|
28
|
+
|
|
29
|
+
if (!wrappingState) return [columnValue, { highestWrappedWidth: isWrapped ? width : 0, isDropdown: isWrapped }]
|
|
30
|
+
|
|
31
|
+
const { highestWrappedWidth, isDropdown: wasDropdown } = wrappingState
|
|
32
|
+
const isDropdown = width <= highestWrappedWidth
|
|
33
|
+
const widthIsLarger = width > highestWrappedWidth
|
|
34
|
+
const largestWidth = Math.max(highestWrappedWidth, width)
|
|
35
|
+
|
|
36
|
+
if ((isDropdown || isWrapped) && !wasDropdown) {
|
|
37
|
+
return [columnValue, { highestWrappedWidth: largestWidth, isDropdown: true }]
|
|
38
|
+
}
|
|
39
|
+
if (wasDropdown && widthIsLarger) {
|
|
40
|
+
return [columnValue, { highestWrappedWidth, isDropdown: false }]
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
.filter(Boolean)
|
|
@@ -16,6 +16,32 @@ describe('handleSorting', () => {
|
|
|
16
16
|
expect(result.values).toEqual(['value1', 'value2', 'value3'])
|
|
17
17
|
})
|
|
18
18
|
|
|
19
|
+
it('should sort orderedValues in asc order if order is asc"', () => {
|
|
20
|
+
const singleFilter = {
|
|
21
|
+
values: ['value3', 'value1', 'value2'],
|
|
22
|
+
orderedValues: ['value2', 'value1', 'value3'],
|
|
23
|
+
order: 'asc',
|
|
24
|
+
filterStyle: 'nested-dropdown'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const result = handleSorting(singleFilter)
|
|
28
|
+
|
|
29
|
+
expect(result.orderedValues).toEqual(['value1', 'value2', 'value3'])
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should sort orderedValues in desc order if order is desc"', () => {
|
|
33
|
+
const singleFilter = {
|
|
34
|
+
values: ['value3', 'value1', 'value2'],
|
|
35
|
+
orderedValues: ['value1', 'value2', 'value1'],
|
|
36
|
+
order: 'desc',
|
|
37
|
+
filterStyle: 'nested-dropdown'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const result = handleSorting(singleFilter)
|
|
41
|
+
|
|
42
|
+
expect(result.orderedValues).toEqual(['value3', 'value2', 'value1'])
|
|
43
|
+
})
|
|
44
|
+
|
|
19
45
|
it('should sort values in ascending order by default', () => {
|
|
20
46
|
const singleFilter = {
|
|
21
47
|
values: ['value3', 'value1', 'value2'],
|
|
@@ -6,12 +6,13 @@ import Loader from '../Loader'
|
|
|
6
6
|
|
|
7
7
|
const Options: React.FC<{
|
|
8
8
|
subOptions: ValueTextPair[]
|
|
9
|
+
handleBlur: React.FocusEventHandler<HTMLLIElement>
|
|
9
10
|
filterIndex: number
|
|
10
11
|
label: string
|
|
11
12
|
handleSubGroupSelect: Function
|
|
12
13
|
userSelectedLabel: string
|
|
13
14
|
userSearchTerm: string
|
|
14
|
-
}> = ({ subOptions, filterIndex, label, handleSubGroupSelect, userSelectedLabel, userSearchTerm }) => {
|
|
15
|
+
}> = ({ subOptions, handleBlur, filterIndex, label, handleSubGroupSelect, userSelectedLabel, userSearchTerm }) => {
|
|
15
16
|
const [isTierOneExpanded, setIsTierOneExpanded] = useState(true)
|
|
16
17
|
const checkMark = <>✔</>
|
|
17
18
|
|
|
@@ -45,6 +46,7 @@ const Options: React.FC<{
|
|
|
45
46
|
tabIndex={0}
|
|
46
47
|
aria-label={label}
|
|
47
48
|
onClick={handleGroupClick}
|
|
49
|
+
onBlur={handleBlur}
|
|
48
50
|
onKeyUp={handleKeyUp}
|
|
49
51
|
className={`nested-dropdown-group-${filterIndex}`}
|
|
50
52
|
>
|
|
@@ -132,6 +134,7 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({
|
|
|
132
134
|
}, [activeGroup, activeSubGroup])
|
|
133
135
|
const [inputHasFocus, setInputHasFocus] = useState(false)
|
|
134
136
|
const [isListOpened, setIsListOpened] = useState(false)
|
|
137
|
+
const nestedDropdownRef = useRef(null)
|
|
135
138
|
const searchInput = useRef(null)
|
|
136
139
|
const searchDropdown = useRef(null)
|
|
137
140
|
|
|
@@ -225,24 +228,34 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({
|
|
|
225
228
|
setUserSearchTerm(newSearchTerm)
|
|
226
229
|
}
|
|
227
230
|
|
|
228
|
-
const handleOnBlur = e => {
|
|
231
|
+
const handleOnBlur = (e: React.FocusEvent<HTMLLIElement, Element>): void => {
|
|
229
232
|
if (
|
|
230
233
|
e.relatedTarget === null ||
|
|
231
234
|
![
|
|
232
235
|
`nested-dropdown-${filterIndex}`,
|
|
233
236
|
`nested-dropdown-group-${filterIndex}`,
|
|
234
|
-
`selectable-item-${filterIndex}
|
|
237
|
+
`selectable-item-${filterIndex}`,
|
|
238
|
+
`main-nested-dropdown-container-${filterIndex}`
|
|
235
239
|
].includes(e.relatedTarget.className)
|
|
236
240
|
) {
|
|
237
241
|
setInputHasFocus(false)
|
|
238
242
|
setIsListOpened(false)
|
|
243
|
+
} else {
|
|
244
|
+
;(e.relatedTarget as HTMLElement).focus()
|
|
239
245
|
}
|
|
240
246
|
}
|
|
241
247
|
|
|
248
|
+
function handleBlur(nestedDropdown, handleOnBlur) {
|
|
249
|
+
nestedDropdown?.addEventListener('blur', handleOnBlur)
|
|
250
|
+
}
|
|
251
|
+
handleBlur(searchInput.current, e => handleOnBlur(e))
|
|
252
|
+
handleBlur(searchDropdown.current, e => handleOnBlur(e))
|
|
253
|
+
|
|
242
254
|
return (
|
|
243
255
|
<>
|
|
244
256
|
<div
|
|
245
257
|
id={dropdownId}
|
|
258
|
+
ref={nestedDropdownRef}
|
|
246
259
|
className={`nested-dropdown nested-dropdown-${filterIndex} ${isListOpened ? 'open-filter' : ''}`}
|
|
247
260
|
onKeyUp={handleKeyUp}
|
|
248
261
|
>
|
|
@@ -268,7 +281,6 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({
|
|
|
268
281
|
if (inputHasFocus) setIsListOpened(!isListOpened)
|
|
269
282
|
}}
|
|
270
283
|
onFocus={() => setInputHasFocus(true)}
|
|
271
|
-
onBlur={e => handleOnBlur(e)}
|
|
272
284
|
/>
|
|
273
285
|
<span className='list-arrow' aria-hidden={true}>
|
|
274
286
|
<Icon display='caretDown' />
|
|
@@ -282,7 +294,7 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({
|
|
|
282
294
|
aria-labelledby='main-nested-dropdown'
|
|
283
295
|
aria-expanded={isListOpened}
|
|
284
296
|
ref={searchDropdown}
|
|
285
|
-
className={`main-nested-dropdown-container
|
|
297
|
+
className={`main-nested-dropdown-container-${filterIndex}${isListOpened ? '' : ' hide'}`}
|
|
286
298
|
>
|
|
287
299
|
{filterOptions.length
|
|
288
300
|
? filterOptions.map(([group, subgroup], index) => {
|
|
@@ -291,6 +303,7 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({
|
|
|
291
303
|
return (
|
|
292
304
|
<Options
|
|
293
305
|
key={groupTextValue + '_' + index}
|
|
306
|
+
handleBlur={handleOnBlur}
|
|
294
307
|
subOptions={subgroup}
|
|
295
308
|
filterIndex={filterIndex}
|
|
296
309
|
label={groupTextValue}
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
[class^='main-nested-dropdown-container-'],
|
|
33
33
|
.nested-dropdown-input-container {
|
|
34
34
|
border: 1px solid var(--cool-gray-10);
|
|
35
35
|
min-width: 200px;
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
&
|
|
78
|
+
& [class^='main-nested-dropdown-container-'] {
|
|
79
79
|
max-height: 375px;
|
|
80
80
|
overflow-y: scroll;
|
|
81
81
|
position: absolute;
|