@cdc/core 4.25.3 → 4.25.6-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 (89) hide show
  1. package/assets/icon-close.svg +1 -1
  2. package/components/Alert/components/Alert.tsx +1 -1
  3. package/components/DataTable/DataTable.tsx +18 -16
  4. package/components/DataTable/DataTableStandAlone.tsx +15 -9
  5. package/components/DataTable/components/CellAnchor.tsx +1 -1
  6. package/components/DataTable/components/ChartHeader.tsx +8 -5
  7. package/components/DataTable/components/DataTableEditorPanel.tsx +25 -3
  8. package/components/DataTable/components/MapHeader.tsx +1 -0
  9. package/components/DataTable/helpers/chartCellMatrix.tsx +14 -10
  10. package/components/DataTable/helpers/getChartCellValue.ts +42 -26
  11. package/components/DataTable/helpers/mapCellMatrix.tsx +25 -7
  12. package/components/DownloadButton.tsx +17 -2
  13. package/components/EditorPanel/DataTableEditor.tsx +1 -1
  14. package/components/EditorPanel/FootnotesEditor.tsx +76 -22
  15. package/components/EditorPanel/Inputs.tsx +12 -4
  16. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +3 -2
  17. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +51 -35
  18. package/components/Filters/Filters.tsx +158 -461
  19. package/components/Filters/components/Dropdown.tsx +39 -0
  20. package/components/Filters/components/Tabs.tsx +82 -0
  21. package/components/Filters/helpers/getChangedFilters.ts +31 -0
  22. package/components/Filters/helpers/getNestedOptions.ts +2 -2
  23. package/components/Filters/helpers/handleSorting.ts +2 -2
  24. package/components/Filters/helpers/tests/getChangedFilters.test.ts +92 -0
  25. package/components/Filters/helpers/tests/getNestedOptions.test.ts +31 -0
  26. package/components/Filters/index.ts +1 -1
  27. package/components/Footnotes/Footnotes.tsx +1 -1
  28. package/components/Footnotes/FootnotesStandAlone.tsx +8 -33
  29. package/components/Layout/components/Visualization/index.tsx +4 -3
  30. package/components/Legend/Legend.Gradient.tsx +68 -24
  31. package/components/MultiSelect/MultiSelect.tsx +3 -6
  32. package/components/MultiSelect/multiselect.styles.css +2 -0
  33. package/components/NestedDropdown/NestedDropdown.tsx +21 -21
  34. package/components/RichTooltip/RichTooltip.tsx +37 -0
  35. package/components/RichTooltip/richTooltip.css +16 -0
  36. package/components/Table/Table.tsx +142 -142
  37. package/components/Table/components/Row.tsx +1 -1
  38. package/components/Table/table.styles.css +10 -0
  39. package/components/_stories/DataTable.stories.tsx +9 -2
  40. package/components/_stories/Table.stories.tsx +1 -1
  41. package/components/_stories/styles.scss +0 -4
  42. package/components/ui/Accordion.jsx +8 -1
  43. package/components/ui/Title/index.tsx +4 -1
  44. package/components/ui/Title/{Title.scss → title.styles.css} +0 -2
  45. package/components/ui/_stories/Colors.stories.mdx +220 -0
  46. package/components/ui/_stories/IconGallery.stories.mdx +14 -0
  47. package/components/ui/_stories/Title.stories.tsx +29 -4
  48. package/components/ui/accordion.styles.css +3 -0
  49. package/data/colorPalettes.js +0 -1
  50. package/dist/cove-main.css +3 -8
  51. package/dist/cove-main.css.map +1 -1
  52. package/helpers/constants.ts +6 -0
  53. package/helpers/cove/accessibility.ts +7 -8
  54. package/helpers/cove/number.ts +5 -3
  55. package/helpers/coveUpdateWorker.ts +9 -1
  56. package/helpers/filterOrderOptions.ts +17 -0
  57. package/helpers/formatConfigBeforeSave.ts +19 -32
  58. package/helpers/isNumber.ts +20 -0
  59. package/helpers/isRightAlignedTableValue.js +1 -1
  60. package/helpers/pivotData.ts +16 -11
  61. package/helpers/tests/pivotData.test.ts +74 -0
  62. package/helpers/updateFieldFactory.ts +1 -0
  63. package/helpers/ver/4.25.3.ts +25 -2
  64. package/helpers/ver/4.25.4.ts +110 -0
  65. package/helpers/ver/4.25.6.ts +36 -0
  66. package/helpers/ver/4.25.7.ts +26 -0
  67. package/helpers/ver/tests/4.25.4.test.ts +89 -0
  68. package/helpers/ver/tests/4.25.6.test.ts +84 -0
  69. package/helpers/viewports.ts +4 -0
  70. package/package.json +7 -6
  71. package/styles/_global-variables.scss +3 -0
  72. package/styles/_global.scss +0 -4
  73. package/styles/_reset.scss +0 -6
  74. package/styles/filters.scss +0 -4
  75. package/styles/v2/main.scss +0 -5
  76. package/types/Axis.ts +2 -0
  77. package/types/DataSet.ts +14 -0
  78. package/types/Footnotes.ts +5 -2
  79. package/types/General.ts +1 -0
  80. package/types/Legend.ts +1 -0
  81. package/types/Table.ts +1 -0
  82. package/types/Visualization.ts +3 -12
  83. package/types/VizFilter.ts +3 -0
  84. package/components/ui/_stories/Colors.stories.tsx +0 -92
  85. package/components/ui/_stories/Icon.stories.tsx +0 -29
  86. package/helpers/cove/fontSettings.ts +0 -2
  87. package/helpers/isNumber.js +0 -24
  88. package/helpers/isNumberLog.js +0 -18
  89. /package/helpers/{fetchRemoteData.js → fetchRemoteData.ts} +0 -0
@@ -0,0 +1,39 @@
1
+ import { VizFilter } from '../../../types/VizFilter'
2
+
3
+ export const DROPDOWN_STYLES = 'py-2 ps-2 w-100 d-block'
4
+
5
+ type DropdownProps = {
6
+ index: number
7
+ label: string
8
+ filter: VizFilter
9
+ changeFilterActive: (index: number, value: string) => void
10
+ }
11
+
12
+ const Dropdown: React.FC<DropdownProps> = ({ index: outerIndex, label, filter, changeFilterActive }) => {
13
+ const { active, queuedActive } = filter
14
+
15
+ return (
16
+ <select
17
+ id={`filter-${outerIndex}`}
18
+ name={label}
19
+ aria-label={`Filter by ${label}`}
20
+ className={`cove-form-select ${DROPDOWN_STYLES}`}
21
+ style={{ backgroundColor: 'white' }}
22
+ data-index='0'
23
+ value={queuedActive || active}
24
+ onChange={e => {
25
+ changeFilterActive(outerIndex, e.target.value)
26
+ }}
27
+ >
28
+ {filter.values?.map((value, index) => {
29
+ return (
30
+ <option key={index} value={value} aria-label={value}>
31
+ {filter.labels && filter.labels[value] ? filter.labels[value] : value}
32
+ </option>
33
+ )
34
+ })}
35
+ </select>
36
+ )
37
+ }
38
+
39
+ export default Dropdown
@@ -0,0 +1,82 @@
1
+ import { useEffect, useId, useState } from 'react'
2
+ import { VizFilter } from '../../../types/VizFilter'
3
+
4
+ type TabsProps = {
5
+ filter: VizFilter
6
+ index: number
7
+ changeFilterActive: Function
8
+ theme: string
9
+ }
10
+
11
+ const Tabs: React.FC<TabsProps> = ({ filter, index: outerIndex, changeFilterActive, theme }) => {
12
+ const [selectedFilter, setSelectedFilter] = useState<EventTarget>(null)
13
+
14
+ const id = useId()
15
+
16
+ useEffect(() => {
17
+ if (selectedFilter) {
18
+ const el = document.getElementById(selectedFilter.id)
19
+ if (el) el.focus()
20
+ }
21
+ }, [selectedFilter])
22
+
23
+ const getClassList = value => {
24
+ const isActive = filter.active === value
25
+ let classList = []
26
+ switch (filter.filterStyle) {
27
+ case 'tab bar':
28
+ classList = ['button__tab-bar', isActive && 'button__tab-bar--active']
29
+ break
30
+ case 'pill':
31
+ classList = ['pill', isActive && 'pill--active', theme && theme]
32
+ break
33
+ default:
34
+ const tabSimple = filter.filterStyle === 'tab-simple' && 'tab--simple'
35
+ classList = ['tab', isActive && 'tab--active', theme && theme, tabSimple]
36
+ break
37
+ }
38
+ return classList.filter(Boolean).join(' ')
39
+ }
40
+
41
+ const Tabs = filter.values.map((value, index) => {
42
+ return (
43
+ <button
44
+ id={`${value}-${outerIndex}-${index}-${id}`}
45
+ className={getClassList(value)}
46
+ onClick={e => {
47
+ changeFilterActive(outerIndex, value)
48
+ setSelectedFilter(e.target)
49
+ }}
50
+ onKeyDown={e => {
51
+ if (e.keyCode === 13) {
52
+ changeFilterActive(outerIndex, value)
53
+ setSelectedFilter(e.target)
54
+ }
55
+ }}
56
+ >
57
+ {value}
58
+ </button>
59
+ )
60
+ })
61
+
62
+ switch (filter.filterStyle) {
63
+ case 'tab bar':
64
+ return <section className='single-filters__tab-bar'>{Tabs}</section>
65
+ case 'tab-simple':
66
+ return <div className='tab-simple-container d-flex w-100'>{Tabs}</div>
67
+ case 'pill':
68
+ return (
69
+ <>
70
+ {Tabs.map((Tab, index) => (
71
+ <div className='pill__wrapper' key={`pill-${index}`}>
72
+ {Tab}
73
+ </div>
74
+ ))}
75
+ </>
76
+ )
77
+ default:
78
+ return <>{Tabs}</>
79
+ }
80
+ }
81
+
82
+ export default Tabs
@@ -0,0 +1,31 @@
1
+ import _ from 'lodash'
2
+ import { getQueryParams, updateQueryString } from '../../../helpers/queryStringUtils'
3
+
4
+ export const getChangedFilters = (filters, index, value, filterBehavior) => {
5
+ const newFilters = _.cloneDeep(filters)
6
+ const newFilter = newFilters[index]
7
+ if (filterBehavior === 'Apply Button') {
8
+ newFilter.queuedActive = value
9
+ } else {
10
+ if (newFilter.filterStyle !== 'nested-dropdown') {
11
+ newFilter.active = value
12
+ } else {
13
+ newFilter.active = value[0]
14
+ newFilter.subGrouping.active = value[1]
15
+ }
16
+
17
+ const queryParams = getQueryParams()
18
+ if (newFilter.setByQueryParameter && queryParams[newFilter.setByQueryParameter] !== newFilter.active) {
19
+ queryParams[newFilter.setByQueryParameter] = newFilter.active
20
+ updateQueryString(queryParams)
21
+ }
22
+ if (
23
+ newFilter?.subGrouping?.setByQueryParameter &&
24
+ queryParams[newFilter?.subGrouping?.setByQueryParameter] !== newFilter?.subGrouping.active
25
+ ) {
26
+ queryParams[newFilter?.subGrouping?.setByQueryParameter] = newFilter.subGrouping.active
27
+ updateQueryString(queryParams)
28
+ }
29
+ }
30
+ return newFilters
31
+ }
@@ -12,7 +12,7 @@ export const getNestedOptions = ({ orderedValues, values, subGrouping }: GetOpti
12
12
  const filteredValues = orderedValues?.length
13
13
  ? orderedValues.filter(orderedValue => values.includes(orderedValue))
14
14
  : values
15
- const v: NestedOptions = filteredValues.map<[ValueTextPair, ValueTextPair[]]>(value => {
15
+ const options: NestedOptions = filteredValues.map<[ValueTextPair, ValueTextPair[]]>(value => {
16
16
  if (!subGrouping) return [[value], []]
17
17
  const { orderedValues, values: filteredSubValues } = subGrouping.valuesLookup[value]
18
18
  // keep custom subFilter order
@@ -25,5 +25,5 @@ export const getNestedOptions = ({ orderedValues, values, subGrouping }: GetOpti
25
25
  return structuredNestedDropdownData
26
26
  })
27
27
 
28
- return v
28
+ return options
29
29
  }
@@ -17,8 +17,8 @@ export const handleSorting = singleFilter => {
17
17
  return String(asc ? a : b).localeCompare(String(asc ? b : a), 'en', { numeric: true })
18
18
  }
19
19
 
20
- singleFilter.values = singleFilterValues.sort(sort)
21
- singleFilter.orderedValues = singleFilterValues.sort(sort)
20
+ singleFilter.values = singleFilterValues?.sort(sort)
21
+ singleFilter.orderedValues = singleFilterValues?.sort(sort)
22
22
 
23
23
  return singleFilter
24
24
  }
@@ -0,0 +1,92 @@
1
+ import { describe, it, expect, vi, type Mock } from 'vitest'
2
+ import { getChangedFilters } from '../getChangedFilters'
3
+ import { getQueryParams, updateQueryString } from '../../../../helpers/queryStringUtils'
4
+ import _ from 'lodash'
5
+
6
+ vi.mock('../../../../helpers/queryStringUtils', () => ({
7
+ getQueryParams: vi.fn(),
8
+ updateQueryString: vi.fn()
9
+ }))
10
+
11
+ describe('getChangedFilters', () => {
12
+ it('should update queuedActive when filterBehavior is "Apply Button"', () => {
13
+ const filters = [{ queuedActive: false }]
14
+ const index = 0
15
+ const value = true
16
+ const filterBehavior = 'Apply Button'
17
+
18
+ const result = getChangedFilters(filters, index, value, filterBehavior)
19
+
20
+ expect(result[index].queuedActive).toBe(true)
21
+ })
22
+
23
+ it('should update active for non-nested-dropdown filters', () => {
24
+ const filters = [{ filterStyle: 'dropdown', active: false }]
25
+ const index = 0
26
+ const value = true
27
+ const filterBehavior = 'Immediate'
28
+
29
+ const result = getChangedFilters(filters, index, value, filterBehavior)
30
+
31
+ expect(result[index].active).toBe(true)
32
+ })
33
+
34
+ it('should update active and subGrouping.active for nested-dropdown filters', () => {
35
+ const filters = [
36
+ {
37
+ filterStyle: 'nested-dropdown',
38
+ active: null,
39
+ subGrouping: { active: null }
40
+ }
41
+ ]
42
+ const index = 0
43
+ const value = ['parentValue', 'childValue']
44
+ const filterBehavior = 'Immediate'
45
+
46
+ const result = getChangedFilters(filters, index, value, filterBehavior)
47
+
48
+ expect(result[index].active).toBe('parentValue')
49
+ expect(result[index].subGrouping.active).toBe('childValue')
50
+ })
51
+
52
+ it('should update query parameters when setByQueryParameter is defined', () => {
53
+ const filters = [
54
+ {
55
+ filterStyle: 'dropdown',
56
+ active: false,
57
+ setByQueryParameter: 'filterParam'
58
+ }
59
+ ]
60
+ const index = 0
61
+ const value = true
62
+ const filterBehavior = 'Immediate'
63
+
64
+ ;(getQueryParams as Mock).mockReturnValue({ filterParam: false })
65
+
66
+ getChangedFilters(filters, index, value, filterBehavior)
67
+
68
+ expect(updateQueryString).toHaveBeenCalledWith({ filterParam: true })
69
+ })
70
+
71
+ it('should update query parameters for subGrouping when setByQueryParameter is defined', () => {
72
+ const filters = [
73
+ {
74
+ filterStyle: 'nested-dropdown',
75
+ active: null,
76
+ subGrouping: {
77
+ active: null,
78
+ setByQueryParameter: 'subFilterParam'
79
+ }
80
+ }
81
+ ]
82
+ const index = 0
83
+ const value = ['parentValue', 'childValue']
84
+ const filterBehavior = 'Immediate'
85
+
86
+ ;(getQueryParams as Mock).mockReturnValue({ subFilterParam: null })
87
+
88
+ getChangedFilters(filters, index, value, filterBehavior)
89
+
90
+ expect(updateQueryString).toHaveBeenCalledWith({ subFilterParam: 'childValue' })
91
+ })
92
+ })
@@ -90,4 +90,35 @@ describe('getNestedOptions', () => {
90
90
  ]
91
91
  expect(getNestedOptions(params)).toEqual(expectedOutput)
92
92
  })
93
+
94
+ it('should return an empty array when values is an empty array', () => {
95
+ const params = {
96
+ values: [],
97
+ subGrouping: null
98
+ }
99
+ const expectedOutput: NestedOptions = []
100
+ expect(getNestedOptions(params)).toEqual(expectedOutput)
101
+ })
102
+
103
+ it('should handle values with a single element', () => {
104
+ const params = {
105
+ values: ['value1'],
106
+ subGrouping: null
107
+ }
108
+ const expectedOutput: NestedOptions = [[['value1'], []]]
109
+ expect(getNestedOptions(params)).toEqual(expectedOutput)
110
+ })
111
+
112
+ it('should handle values with multiple elements', () => {
113
+ const params = {
114
+ values: ['value1', 'value2', 'value3'],
115
+ subGrouping: null
116
+ }
117
+ const expectedOutput: NestedOptions = [
118
+ [['value1'], []],
119
+ [['value2'], []],
120
+ [['value3'], []]
121
+ ]
122
+ expect(getNestedOptions(params)).toEqual(expectedOutput)
123
+ })
93
124
  })
@@ -1,5 +1,5 @@
1
1
  export { default } from './Filters'
2
2
 
3
- export { filterOrderOptions, filterStyleOptions, useFilters } from './Filters'
3
+ export { filterStyleOptions } from './Filters'
4
4
 
5
5
  export { handleSorting } from './helpers/handleSorting'
@@ -11,7 +11,7 @@ const Footnotes: React.FC<FootnotesProps> = ({ footnotes }) => {
11
11
  <ul className='cove-footnotes'>
12
12
  {footnotes.map((note, i) => {
13
13
  return (
14
- <li key={note.symbol + i} className='mb-1'>
14
+ <li key={`${note.symbol || 'footnote-'}${i}`} className='mb-1'>
15
15
  {note.symbol && <span className='me-1'>{note.symbol}</span>}
16
16
  {note.text}
17
17
  </li>
@@ -1,45 +1,20 @@
1
- import EditorWrapper from '../EditorWrapper'
2
1
  import Footnotes from './Footnotes'
3
- import FootnotesEditor from '../EditorPanel/FootnotesEditor'
4
- import { ViewPort } from '../../types/ViewPort'
5
- import FootnotesConfig, { Footnote } from '../../types/Footnotes'
2
+ import FootnotesConfig from '../../types/Footnotes'
6
3
  import _ from 'lodash'
7
4
  import { useMemo } from 'react'
8
- import { updateFieldFactory } from '../../helpers/updateFieldFactory'
5
+ import { filterVizData } from '../../helpers/filterVizData'
6
+ import { VizFilter } from '../../types/VizFilter'
9
7
 
10
8
  type StandAloneProps = {
11
- isEditor?: boolean
12
- visualizationKey: string
13
9
  config: FootnotesConfig
14
- updateConfig?: (config: FootnotesConfig) => void
15
- viewport?: ViewPort
10
+ filters?: VizFilter[]
16
11
  }
17
12
 
18
- const FootnotesStandAlone: React.FC<StandAloneProps> = ({
19
- visualizationKey,
20
- config,
21
- viewport,
22
- isEditor,
23
- updateConfig
24
- }) => {
25
- const updateField = updateFieldFactory<Footnote[]>(config, updateConfig)
26
- if (isEditor)
27
- return (
28
- <EditorWrapper
29
- component={FootnotesStandAlone}
30
- visualizationKey={visualizationKey}
31
- visualizationConfig={config}
32
- updateConfig={updateConfig}
33
- type={'Footnotes'}
34
- viewport={viewport}
35
- >
36
- <FootnotesEditor key={visualizationKey} config={config} updateField={updateField} />
37
- </EditorWrapper>
38
- )
39
-
13
+ const FootnotesStandAlone: React.FC<StandAloneProps> = ({ config, filters }) => {
14
+ if (!config) return null
40
15
  // get the api footnotes from the config
41
16
  const apiFootnotes = useMemo(() => {
42
- const configData = config.formattedData || config.data
17
+ const configData = filterVizData(filters, config.data)
43
18
  if (configData && config.dataKey && config.dynamicFootnotes) {
44
19
  const { symbolColumn, textColumn, orderColumn } = config.dynamicFootnotes
45
20
  const _data = configData.map(row => _.pick(row, [symbolColumn, textColumn, orderColumn]))
@@ -47,7 +22,7 @@ const FootnotesStandAlone: React.FC<StandAloneProps> = ({
47
22
  return _data.map(row => ({ symbol: row[symbolColumn], text: row[textColumn] }))
48
23
  }
49
24
  return []
50
- }, [config.dynamicFootnotes, config.formattedData, config.data])
25
+ }, [config.dynamicFootnotes, config.data, filters])
51
26
 
52
27
  // get static footnotes from the config.footnotes
53
28
  const staticFootnotes = config.staticFootnotes || []
@@ -2,21 +2,23 @@
2
2
  import { ChartConfig } from '@cdc/chart/src/types/ChartConfig'
3
3
  import React, { forwardRef } from 'react'
4
4
  import { Config as DataBiteConfig } from '@cdc/data-bite/src/types/Config'
5
+ import { Config as DataTableConfig } from '@cdc/data-table/src/types/Config'
5
6
  import './visualizations.scss'
6
7
  import { Config as WaffleChartConfig } from '@cdc/waffle-chart/src/types/Config'
7
8
  import { MarkupIncludeConfig } from '@cdc/core/types/MarkupInclude'
8
9
  import { DashboardFilters } from '@cdc/dashboard/src/types/DashboardFilters'
10
+ import { MapConfig } from '@cdc/map/src/types/MapConfig'
9
11
 
10
12
  type VisualizationWrapper = {
11
13
  children: React.ReactNode
12
- config: ChartConfig | DataBiteConfig | WaffleChartConfig | MarkupIncludeConfig | DashboardFilters
14
+ config: ChartConfig | DataBiteConfig | WaffleChartConfig | MarkupIncludeConfig | DashboardFilters | MapConfig | DataTableConfig
13
15
  currentViewport?: string
14
16
  imageId?: string
15
17
  isEditor: boolean
16
18
  showEditorPanel?: boolean
17
19
  }
18
20
 
19
- const Visualization: React.FC<VisualizationWrapper> = forwardRef((props, ref) => {
21
+ const Visualization = forwardRef<HTMLDivElement, VisualizationWrapper>((props, ref) => {
20
22
  const {
21
23
  config = {},
22
24
  isEditor = false,
@@ -93,7 +95,6 @@ const Visualization: React.FC<VisualizationWrapper> = forwardRef((props, ref) =>
93
95
  }
94
96
 
95
97
  return (
96
- // prettier-ignore
97
98
  <div
98
99
  {...(config.type === 'chart' ? { 'data-lollipop': config.isLollipopChart } : {})}
99
100
  className={getWrappingClasses().join(' ')}
@@ -4,9 +4,11 @@ import { type MapConfig } from '@cdc/map/src/types/MapConfig'
4
4
  import { type ChartConfig } from '@cdc/chart/src/types/ChartConfig'
5
5
  import { getTextWidth } from '../../helpers/getTextWidth'
6
6
  import { DimensionsType } from '../../types/Dimensions'
7
+ import useLegendSeparators from '@cdc/map/src/hooks/useLegendSeparators'
7
8
 
8
9
  const MARGIN = 1
9
10
  const BORDER_SIZE = 1
11
+ const BORDER_COLOR = '#d3d3d3'
10
12
  const MOBILE_BREAKPOINT = 576
11
13
 
12
14
  type CombinedConfig = MapConfig | ChartConfig
@@ -27,7 +29,7 @@ const LegendGradient = ({
27
29
  parentPaddingToSubtract = 0
28
30
  }: GradientProps): JSX.Element => {
29
31
  const { uid, legend, type } = config
30
- const { tickRotation, position, style, subStyle, hideBorder } = legend
32
+ const { tickRotation, position, style, subStyle, separators } = legend
31
33
 
32
34
  const isLinearBlocks = subStyle === 'linear blocks'
33
35
  let [width] = dimensions
@@ -36,6 +38,10 @@ const LegendGradient = ({
36
38
  const legendWidth = Number(width) - parentPaddingToSubtract - MARGIN * 2 - BORDER_SIZE * 2
37
39
  const uniqueID = `${uid}-${Date.now()}`
38
40
 
41
+ // Legend separators logic
42
+ const { legendSeparators, separatorSize, legendSeparatorsToSubtract, getTickSeparatorsAdjustment } =
43
+ useLegendSeparators(separators, legendWidth, isLinearBlocks)
44
+
39
45
  const numTicks = colors?.length
40
46
 
41
47
  const longestLabel = (labels || []).reduce((a: string, b) => (a.length > String(b).length ? a : b), '')
@@ -57,8 +63,8 @@ const LegendGradient = ({
57
63
 
58
64
  // render ticks and labels
59
65
  const ticks = labels.map((key, index) => {
60
- const segmentWidth = legendWidth / numTicks
61
- const xPositionX = index * segmentWidth + segmentWidth + MARGIN
66
+ const segmentWidth = (legendWidth - legendSeparatorsToSubtract) / numTicks
67
+ const xPositionX = index * segmentWidth + segmentWidth + MARGIN + getTickSeparatorsAdjustment(index)
62
68
  const textAnchor = rotationAngle ? 'end' : 'middle'
63
69
  const verticalAnchor = rotationAngle ? 'middle' : 'start'
64
70
  const lastTick = index === labels.length - 1
@@ -92,9 +98,10 @@ const LegendGradient = ({
92
98
 
93
99
  if (style === 'gradient') {
94
100
  return (
95
- <svg className={'w-100 overflow-visible'} height={newHeight}>
101
+ // TODO: figure out why bootstrap 'overflow: visible' is not working consistently
102
+ <svg className={'w-100 overflow-visible'} height={newHeight} style={{ overflow: 'visible' }} width={width}>
96
103
  {/* background border*/}
97
- <rect x={0} y={0} width={legendWidth + MARGIN * 2} height={boxHeight + MARGIN * 2} fill='#d3d3d3' />
104
+ <rect x={0} y={0} width={legendWidth + MARGIN * 2} height={boxHeight + MARGIN * 2} fill={BORDER_COLOR} />
98
105
  {/* Define the gradient */}
99
106
  <linearGradient id={`gradient-smooth-${uniqueID}`} x1='0%' y1='0%' x2='100%' y2='0%'>
100
107
  {stops}
@@ -110,25 +117,62 @@ const LegendGradient = ({
110
117
  />
111
118
  )}
112
119
 
113
- {subStyle === 'linear blocks' &&
114
- colors.map((color, index) => {
115
- const segmentWidth = legendWidth / numTicks
116
- const xPosition = index * segmentWidth + MARGIN
117
- return (
118
- <Group>
119
- <rect
120
- key={index}
121
- x={xPosition}
122
- y={MARGIN}
123
- width={segmentWidth}
124
- height={boxHeight}
125
- fill={color}
126
- stroke='white'
127
- strokeWidth='0'
128
- />
129
- </Group>
130
- )
131
- })}
120
+ {subStyle === 'linear blocks' && (
121
+ <>
122
+ {colors.map((color, index) => {
123
+ const segmentWidth = (legendWidth - legendSeparatorsToSubtract) / numTicks
124
+ const xPosition = index * segmentWidth + MARGIN + getTickSeparatorsAdjustment(index)
125
+ return (
126
+ <Group>
127
+ <rect
128
+ key={index}
129
+ x={xPosition}
130
+ y={MARGIN}
131
+ width={segmentWidth}
132
+ height={boxHeight}
133
+ fill={color}
134
+ stroke='white'
135
+ strokeWidth='0'
136
+ />
137
+ </Group>
138
+ )
139
+ })}
140
+ {/* Legend separators */}
141
+ {legendSeparators.map((separatorAfter, index) => {
142
+ const segmentWidth = (legendWidth - legendSeparatorsToSubtract) / numTicks
143
+ const xPosition = separatorAfter * segmentWidth + MARGIN + getTickSeparatorsAdjustment(separatorAfter - 1)
144
+ return (
145
+ <Group>
146
+ {/* Separators block */}
147
+ <rect
148
+ key={index}
149
+ x={xPosition}
150
+ y={MARGIN / 2}
151
+ width={separatorSize}
152
+ height={boxHeight + MARGIN}
153
+ fill={'white'}
154
+ stroke={'white'}
155
+ strokeWidth={MARGIN}
156
+ />
157
+
158
+ {/* Dotted dividing line */}
159
+ <line
160
+ key={index}
161
+ x1={xPosition + separatorSize / 2}
162
+ x2={xPosition + separatorSize / 2}
163
+ y1={-3}
164
+ y2={boxHeight + MARGIN + 3}
165
+ stroke={'var(--colors-gray-cool-40,#8d9297)'}
166
+ strokeWidth={1}
167
+ strokeDasharray='5,3'
168
+ strokeDashoffset={1}
169
+ />
170
+ </Group>
171
+ )
172
+ })}
173
+ </>
174
+ )}
175
+
132
176
  {/* Ticks and labels */}
133
177
  <g>{ticks}</g>
134
178
  </svg>
@@ -36,9 +36,7 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
36
36
  tooltip,
37
37
  loading
38
38
  }) => {
39
- const preselectedItems = useMemo(() => options.filter(opt => selected.includes(opt.value)).slice(0, limit), [options])
40
- const [selectedItems, setSelectedItems] = useState<Option[]>()
41
- const items = selectedItems || preselectedItems
39
+ const items = useMemo(() => options.filter(opt => selected.includes(opt.value)).slice(0, limit), [options])
42
40
  const [expanded, setExpanded] = useState(false)
43
41
  const multiSelectRef = useRef(null)
44
42
 
@@ -68,14 +66,12 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
68
66
  if (e && e.type === 'keyup' && e.key !== 'Enter') return
69
67
  if (limit && items?.length >= limit) return
70
68
  const newItems = [...items, option]
71
- setSelectedItems(newItems)
72
69
  update(newItems)
73
70
  }
74
71
 
75
72
  const handleItemRemove = (option: Option, e = null) => {
76
73
  if (e && e.type === 'keyup' && e.key !== 'Enter') return
77
74
  const newItems = items.filter(item => item.value !== option.value)
78
- setSelectedItems(newItems)
79
75
  update(newItems)
80
76
  }
81
77
 
@@ -99,6 +95,7 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
99
95
  items.map(item => (
100
96
  <div key={item.value} aria-labelledby={label ? multiID + label : undefined}>
101
97
  {item.label}
98
+
102
99
  <button
103
100
  aria-label='Remove'
104
101
  onClick={e => {
@@ -114,7 +111,7 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
114
111
  </div>
115
112
  ))
116
113
  ) : (
117
- <span className='pl-1 pt-1'>{loading ? 'Loading...' : '- Select -'}</span>
114
+ <span className='ps-1 pt-1'>{loading ? 'Loading...' : '- Select -'}</span>
118
115
  )}
119
116
  <button
120
117
  aria-label={expanded ? 'Collapse' : 'Expand'}
@@ -5,6 +5,7 @@
5
5
  .wrapper {
6
6
  display: inline-flex;
7
7
  align-items: center;
8
+ width: 100%;
8
9
  .selected {
9
10
  &[aria-disabled='true'] {
10
11
  background: var(--lightestGray);
@@ -12,6 +13,7 @@
12
13
  border: 1px solid var(--lightGray);
13
14
  padding: 7px;
14
15
  min-width: 200px;
16
+ width: 100%;
15
17
  display: inline-block;
16
18
  :is(button) {
17
19
  border: none;