@cdc/core 4.25.1 → 4.25.3-6

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.
@@ -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.geoType]} figure.`
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
  }
@@ -211,7 +212,12 @@ const DataTable = (props: DataTableProps) => {
211
212
  const getClassNames = (): string => {
212
213
  const classes = ['data-table-container']
213
214
 
214
- if (config.table.showDownloadLinkBelow) {
215
+ const hasDownloadLinkAbove =
216
+ (config.table.download || config.general?.showDownloadButton) && !config.table.showDownloadLinkBelow
217
+
218
+ const isStandaloneTable = config.type === 'table'
219
+
220
+ if (!hasDownloadLinkAbove && !isStandaloneTable) {
215
221
  classes.push('mt-4')
216
222
  }
217
223
 
@@ -236,24 +242,45 @@ const DataTable = (props: DataTableProps) => {
236
242
  }
237
243
  }
238
244
 
239
- const getMediaControlsClasses = belowTable => {
245
+ const getMediaControlsClasses = (belowTable, hasDownloadLink) => {
240
246
  const classes = ['download-links']
241
247
  if (!belowTable) {
242
- classes.push('mt-4', 'mb-2')
248
+ if (hasDownloadLink) {
249
+ classes.push('mt-4', 'mb-2')
250
+ }
243
251
  const isLegendOnBottom = config?.legend?.position === 'bottom' || isLegendWrapViewport(viewport)
244
252
  if (config.brush?.active && !isLegendOnBottom) classes.push('brush-active')
245
253
  if (config.brush?.active && config.legend.hide) classes.push('brush-active')
246
254
  } else {
247
- classes.push('mt-2')
255
+ if (hasDownloadLink) {
256
+ classes.push('mt-2')
257
+ }
248
258
  }
249
259
  return classes
250
260
  }
251
261
 
262
+ const childrenMatrix =
263
+ config.type === 'map'
264
+ ? mapCellMatrix({ rows, wrapColumns, ...props, runtimeData, viewport })
265
+ : chartCellMatrix({ rows, ...props, runtimeData, isVertical, sortBy, hasRowType, viewport })
266
+
267
+ // If every value in a column is a number, record the column index so the header and cells can be right-aligned
268
+ const rightAlignedCols = childrenMatrix.length
269
+ ? Object.fromEntries(
270
+ Object.keys(childrenMatrix[0])
271
+ .filter(
272
+ i => childrenMatrix.filter(row => isRightAlignedTableValue(row[i])).length === childrenMatrix.length
273
+ )
274
+ .map(x => [x, true])
275
+ )
276
+ : {}
277
+
252
278
  const TableMediaControls = ({ belowTable }) => {
279
+ const hasDownloadLink = config.table.download || config.general?.showDownloadButton
253
280
  return (
254
- <MediaControls.Section classes={getMediaControlsClasses(belowTable)}>
281
+ <MediaControls.Section classes={getMediaControlsClasses(belowTable, hasDownloadLink)}>
255
282
  <MediaControls.Link config={config} dashboardDataConfig={dataConfig} />
256
- {(config.table.download || config.general?.showDownloadButton) && (
283
+ {hasDownloadLink && (
257
284
  <DownloadButton
258
285
  rawData={getDownloadData()}
259
286
  fileName={`${vizTitle || 'data-table'}.csv`}
@@ -278,18 +305,20 @@ const DataTable = (props: DataTableProps) => {
278
305
  viewport={viewport}
279
306
  wrapColumns={wrapColumns}
280
307
  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
- }
308
+ childrenMatrix={childrenMatrix}
286
309
  tableName={config.type}
287
310
  caption={caption}
288
311
  stickyHeader
289
312
  hasRowType={hasRowType}
290
313
  headContent={
291
314
  config.type === 'map' ? (
292
- <MapHeader columns={columns} {...props} sortBy={sortBy} setSortBy={setSortBy} />
315
+ <MapHeader
316
+ columns={columns}
317
+ {...props}
318
+ sortBy={sortBy}
319
+ setSortBy={setSortBy}
320
+ rightAlignedCols={rightAlignedCols}
321
+ />
293
322
  ) : (
294
323
  <ChartHeader
295
324
  data={runtimeData}
@@ -299,6 +328,7 @@ const DataTable = (props: DataTableProps) => {
299
328
  sortBy={sortBy}
300
329
  setSortBy={setSortBy}
301
330
  viewport={viewport}
331
+ rightAlignedCols={rightAlignedCols}
302
332
  />
303
333
  )
304
334
  }
@@ -310,6 +340,7 @@ const DataTable = (props: DataTableProps) => {
310
340
  'aria-rowcount': config?.data?.length ? config.data.length : -1,
311
341
  hidden: !expanded
312
342
  }}
343
+ rightAlignedCols={rightAlignedCols}
313
344
  />
314
345
 
315
346
  {/* REGION Data Table */}
@@ -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 = ({ data, isVertical, config, setSortBy, sortBy, hasRowType, viewport }: ChartHeaderProps) => {
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={{ minWidth: (config.table.cellMinWidth || 0) + 'px' }}
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
- {column === sortBy.column && !hasRowType && <SortIcon ascending={sortByAsc} />}
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={{ minWidth: (config.table.cellMinWidth || 0) + 'px' }}
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
- {index === sortBy.colIndex && !hasRowType && <SortIcon ascending={sortByAsc} />}
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}
@@ -83,7 +83,7 @@ table.data-table {
83
83
  }
84
84
  th,
85
85
  td {
86
- padding: 0.5em 1.3em 0.5em 0.7em;
86
+ padding: 0.5em 0.7em;
87
87
  line-height: normal;
88
88
  position: relative;
89
89
  text-align: left;
@@ -16,7 +16,15 @@ type ChartRowsProps = DataTableProps & {
16
16
  hasRowType?: boolean
17
17
  }
18
18
 
19
- const chartCellArray = ({ rows, runtimeData, config, isVertical, sortBy, colorScale, hasRowType }: ChartRowsProps): CellMatrix | GroupCellMatrix => {
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: GroupCellMatrix = {}
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[groupKey]) {
53
- cellMatrix[groupKey] = [groupValues]
60
+ if (!cellMatrix.has(groupKey)) {
61
+ cellMatrix.set(groupKey, [groupValues])
54
62
  } else {
55
- cellMatrix[groupKey].push(groupValues)
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 { useId } from 'react'
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
- if (Number(dimensions[0]) < 768 && filters?.length > 0) {
301
- setMobileFilterStyle(true)
302
- } else {
303
- setMobileFilterStyle(false)
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 pillClassList = ['pill', active === filterOption ? 'pill--active' : null, theme && theme]
385
- const tabClassList = ['tab', active === filterOption && 'tab--active', theme && theme]
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='font-weight-bold mb-2' htmlFor={`filter-${outerIndex}`}>
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)
@@ -18,6 +18,7 @@ export const handleSorting = singleFilter => {
18
18
  }
19
19
 
20
20
  singleFilter.values = singleFilterValues.sort(sort)
21
+ singleFilter.orderedValues = singleFilterValues.sort(sort)
21
22
 
22
23
  return singleFilter
23
24
  }
@@ -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 = <>&#10004;</>
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,10 +281,13 @@ 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
- <Icon display='caretDown' />
286
+ {isListOpened ? (
287
+ <Icon display='caretUp' alt='arrow pointing up' />
288
+ ) : (
289
+ <Icon display='caretDown' alt='arrow pointing down' />
290
+ )}
275
291
  </span>
276
292
  </div>
277
293
  {loading && <Loader spinnerType={'text-secondary'} />}
@@ -282,7 +298,7 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({
282
298
  aria-labelledby='main-nested-dropdown'
283
299
  aria-expanded={isListOpened}
284
300
  ref={searchDropdown}
285
- className={`main-nested-dropdown-container ${isListOpened ? '' : 'hide'}`}
301
+ className={`main-nested-dropdown-container-${filterIndex}${isListOpened ? '' : ' hide'}`}
286
302
  >
287
303
  {filterOptions.length
288
304
  ? filterOptions.map(([group, subgroup], index) => {
@@ -291,6 +307,7 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({
291
307
  return (
292
308
  <Options
293
309
  key={groupTextValue + '_' + index}
310
+ handleBlur={handleOnBlur}
294
311
  subOptions={subgroup}
295
312
  filterIndex={filterIndex}
296
313
  label={groupTextValue}