@cdc/core 4.24.9 → 4.24.11

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 (106) hide show
  1. package/LICENSE +201 -0
  2. package/assets/icon-combo-chart.svg +1 -0
  3. package/assets/icon-epi-chart.svg +27 -0
  4. package/components/AdvancedEditor/AdvancedEditor.tsx +17 -13
  5. package/components/Alert/components/Alert.tsx +34 -8
  6. package/components/BlurStrokeText.tsx +44 -0
  7. package/components/DataTable/DataTable.tsx +62 -36
  8. package/components/DataTable/DataTableStandAlone.tsx +37 -6
  9. package/components/DataTable/components/ChartHeader.tsx +31 -26
  10. package/components/DataTable/components/MapHeader.tsx +19 -10
  11. package/components/DataTable/components/SortIcon/index.tsx +25 -0
  12. package/components/DataTable/components/SortIcon/sort-icon.css +21 -0
  13. package/{styles/_data-table.scss → components/DataTable/data-table.css} +250 -298
  14. package/components/DataTable/helpers/boxplotCellMatrix.tsx +14 -13
  15. package/components/DataTable/helpers/customSort.ts +11 -15
  16. package/components/DataTable/helpers/getChartCellValue.ts +23 -5
  17. package/components/DataTable/helpers/getDataSeriesColumns.ts +5 -1
  18. package/components/DataTable/helpers/getNewSortBy.ts +35 -0
  19. package/components/DataTable/helpers/tests/customSort.test.ts +52 -0
  20. package/components/DataTable/helpers/tests/getNewSortBy.test.ts +26 -0
  21. package/components/EditorPanel/ColumnsEditor.tsx +81 -36
  22. package/components/EditorPanel/DataTableEditor.tsx +149 -43
  23. package/components/EditorPanel/FieldSetWrapper.tsx +2 -2
  24. package/components/EditorPanel/Inputs.tsx +68 -20
  25. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +25 -7
  26. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +30 -55
  27. package/components/{Filters.tsx → Filters/Filters.tsx} +60 -43
  28. package/components/Filters/helpers/applyQueuedActive.ts +12 -0
  29. package/components/Filters/helpers/getNestedOptions.ts +29 -0
  30. package/components/Filters/helpers/handleSorting.ts +18 -0
  31. package/components/Filters/helpers/tests/applyQueuedActive.test.ts +49 -0
  32. package/components/Filters/helpers/tests/getNestedOptions.test.ts +93 -0
  33. package/components/Filters/helpers/tests/handleSorting.test.ts +68 -0
  34. package/components/Filters/index.ts +5 -0
  35. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +1 -7
  36. package/components/Layout/components/Visualization/visualizations.scss +1 -1
  37. package/components/Legend/Legend.Gradient.tsx +44 -36
  38. package/components/Loader/Loader.tsx +33 -0
  39. package/components/Loader/index.ts +1 -0
  40. package/components/Loader/loader.styles.css +13 -0
  41. package/components/MultiSelect/MultiSelect.tsx +85 -62
  42. package/components/MultiSelect/multiselect.styles.css +10 -7
  43. package/components/NestedDropdown/NestedDropdown.tsx +118 -56
  44. package/components/NestedDropdown/nestedDropdownHelpers.ts +34 -0
  45. package/components/NestedDropdown/nesteddropdown.styles.css +22 -13
  46. package/components/NestedDropdown/tests/nestedDropdownHelpers.test.ts +58 -0
  47. package/components/Table/Table.tsx +102 -34
  48. package/components/Table/components/GroupRow.tsx +1 -1
  49. package/components/_stories/BlurStrokeTest.stories.tsx +27 -0
  50. package/components/_stories/DataTable.stories.tsx +14 -0
  51. package/components/_stories/Filters.stories.tsx +57 -0
  52. package/components/_stories/NestedDropdown.stories.tsx +22 -46
  53. package/components/_stories/_mocks/DataTable/no-data.json +108 -0
  54. package/components/_stories/_mocks/nested-dropdown.json +30 -0
  55. package/components/_stories/styles.scss +0 -1
  56. package/components/ui/Icon.tsx +19 -6
  57. package/components/ui/{Tooltip.jsx → Tooltip.tsx} +38 -14
  58. package/data/colorPalettes.js +107 -10
  59. package/dist/cove-main.css +6080 -0
  60. package/dist/cove-main.css.map +1 -0
  61. package/helpers/DataTransform.ts +2 -1
  62. package/helpers/addValuesToFilters.ts +8 -3
  63. package/helpers/cove/{number.js → number.ts} +62 -27
  64. package/helpers/coveUpdateWorker.ts +6 -7
  65. package/helpers/fetchRemoteData.js +32 -37
  66. package/helpers/formatConfigBeforeSave.ts +17 -1
  67. package/helpers/gatherQueryParams.ts +12 -2
  68. package/helpers/pivotData.ts +52 -11
  69. package/helpers/queryStringUtils.ts +6 -0
  70. package/helpers/tests/gatherQueryParams.test.ts +34 -0
  71. package/helpers/tests/pivotData.test.ts +50 -0
  72. package/helpers/useDataVizClasses.ts +42 -20
  73. package/helpers/ver/4.24.10.ts +47 -0
  74. package/helpers/ver/4.24.9.ts +0 -3
  75. package/helpers/ver/tests/4.24.10.test.ts +45 -0
  76. package/helpers/viewports.ts +9 -0
  77. package/package.json +7 -3
  78. package/styles/_button-section.scss +5 -1
  79. package/styles/_global-variables.scss +20 -2
  80. package/styles/_global.scss +22 -30
  81. package/styles/_reset.scss +2 -26
  82. package/styles/base.scss +0 -1
  83. package/styles/cove-main.scss +6 -0
  84. package/styles/filters.scss +6 -26
  85. package/styles/v2/base/_reset.scss +0 -7
  86. package/styles/v2/components/editor.scss +0 -4
  87. package/styles/v2/components/icon.scss +1 -1
  88. package/styles/v2/components/ui/tooltip.scss +42 -40
  89. package/styles/v2/layout/_component.scss +0 -6
  90. package/styles/v2/layout/index.scss +0 -1
  91. package/types/Axis.ts +4 -0
  92. package/types/BoxPlot.ts +5 -3
  93. package/types/Color.ts +1 -1
  94. package/types/General.ts +1 -0
  95. package/types/Legend.ts +1 -2
  96. package/types/MarkupInclude.ts +1 -0
  97. package/types/Runtime.ts +3 -1
  98. package/types/Series.ts +8 -1
  99. package/types/Table.ts +3 -2
  100. package/types/Visualization.ts +19 -8
  101. package/types/VizFilter.ts +2 -1
  102. package/components/DataTable/components/Icons.tsx +0 -10
  103. package/components/_stories/EditorPanel.stories.tsx +0 -54
  104. package/components/_stories/Layout.Debug.stories.tsx +0 -91
  105. package/components/ui/Select.jsx +0 -30
  106. package/helpers/getGradientLegendWidth.ts +0 -15
@@ -0,0 +1,93 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { getNestedOptions } from '../getNestedOptions'
3
+ import { SubGrouping } from '../../../../types/VizFilter'
4
+ import { NestedOptions } from '../../../NestedDropdown/nestedDropdownHelpers'
5
+
6
+ describe('getNestedOptions', () => {
7
+ it('should return nested options when orderedValues is not provided', () => {
8
+ const params = {
9
+ values: ['value1', 'value2'],
10
+ subGrouping: null
11
+ }
12
+ const expectedOutput: NestedOptions = [
13
+ [['value1'], []],
14
+ [['value2'], []]
15
+ ]
16
+ expect(getNestedOptions(params)).toEqual(expectedOutput)
17
+ })
18
+
19
+ it('should return nested options when orderedValues is provided', () => {
20
+ const params = {
21
+ orderedValues: ['value2', 'value1'],
22
+ values: ['value1', 'value2'],
23
+ subGrouping: null
24
+ }
25
+ const expectedOutput: NestedOptions = [
26
+ [['value2'], []],
27
+ [['value1'], []]
28
+ ]
29
+ expect(getNestedOptions(params)).toEqual(expectedOutput)
30
+ })
31
+
32
+ it('should return nested options when subGrouping is not provided', () => {
33
+ const params = {
34
+ values: ['value1', 'value2'],
35
+ subGrouping: null
36
+ }
37
+ const expectedOutput: NestedOptions = [
38
+ [['value1'], []],
39
+ [['value2'], []]
40
+ ]
41
+ expect(getNestedOptions(params)).toEqual(expectedOutput)
42
+ })
43
+
44
+ it('should return nested options when subGrouping is provided with nested values', () => {
45
+ const subGrouping: SubGrouping = {
46
+ valuesLookup: {
47
+ value1: {
48
+ orderedValues: ['subValue2', 'subValue1'],
49
+ values: ['subValue1', 'subValue2']
50
+ },
51
+ value2: {
52
+ orderedValues: null,
53
+ values: ['subValue3']
54
+ }
55
+ }
56
+ }
57
+ const params = {
58
+ orderedValues: ['value1', 'value2'],
59
+ values: ['value1', 'value2'],
60
+ subGrouping
61
+ }
62
+ const expectedOutput: NestedOptions = [
63
+ [['value1'], [['subValue2'], ['subValue1']]],
64
+ [['value2'], [['subValue3']]]
65
+ ]
66
+ expect(getNestedOptions(params)).toEqual(expectedOutput)
67
+ })
68
+
69
+ it('should return nested options when subGrouping is provided without orderedValues', () => {
70
+ const subGrouping: SubGrouping = {
71
+ valuesLookup: {
72
+ value1: {
73
+ orderedValues: null,
74
+ values: ['subValue1', 'subValue2']
75
+ },
76
+ value2: {
77
+ orderedValues: null,
78
+ values: ['subValue3']
79
+ }
80
+ }
81
+ }
82
+ const params = {
83
+ orderedValues: ['value1', 'value2'],
84
+ values: ['value1', 'value2'],
85
+ subGrouping
86
+ }
87
+ const expectedOutput: NestedOptions = [
88
+ [['value1'], [['subValue1'], ['subValue2']]],
89
+ [['value2'], [['subValue3']]]
90
+ ]
91
+ expect(getNestedOptions(params)).toEqual(expectedOutput)
92
+ })
93
+ })
@@ -0,0 +1,68 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { handleSorting } from '../handleSorting'
3
+ import _ from 'lodash'
4
+
5
+ describe('handleSorting', () => {
6
+ it('should use orderedValues when order is "cust" and filterStyle is not "nested-dropdown"', () => {
7
+ const singleFilter = {
8
+ values: ['value3', 'value1', 'value2'],
9
+ orderedValues: ['value1', 'value2', 'value3'],
10
+ order: 'cust',
11
+ filterStyle: 'someOtherStyle'
12
+ }
13
+
14
+ const result = handleSorting(singleFilter)
15
+
16
+ expect(result.values).toEqual(['value1', 'value2', 'value3'])
17
+ })
18
+
19
+ it('should sort values in ascending order by default', () => {
20
+ const singleFilter = {
21
+ values: ['value3', 'value1', 'value2'],
22
+ order: 'asc',
23
+ filterStyle: 'someOtherStyle'
24
+ }
25
+
26
+ const result = handleSorting(singleFilter)
27
+
28
+ expect(result.values).toEqual(['value1', 'value2', 'value3'])
29
+ })
30
+
31
+ it('should sort values in descending order when order is "desc"', () => {
32
+ const singleFilter = {
33
+ values: ['value3', 'value1', 'value2'],
34
+ order: 'desc',
35
+ filterStyle: 'someOtherStyle'
36
+ }
37
+
38
+ const result = handleSorting(singleFilter)
39
+
40
+ expect(result.values).toEqual(['value3', 'value2', 'value1'])
41
+ })
42
+
43
+ it('should not use orderedValues when filterStyle is "nested-dropdown"', () => {
44
+ const singleFilter = {
45
+ values: ['value3', 'value1', 'value2'],
46
+ orderedValues: ['value1', 'value2', 'value3'],
47
+ order: 'cust',
48
+ filterStyle: 'nested-dropdown'
49
+ }
50
+
51
+ const result = handleSorting(singleFilter)
52
+
53
+ expect(result.values).toEqual(['value1', 'value2', 'value3'])
54
+ })
55
+
56
+ it('should handle empty orderedValues when order is "cust"', () => {
57
+ const singleFilter = {
58
+ values: ['value3', 'value1', 'value2'],
59
+ orderedValues: [],
60
+ order: 'cust',
61
+ filterStyle: 'someOtherStyle'
62
+ }
63
+
64
+ const result = handleSorting(singleFilter)
65
+
66
+ expect(result.values).toEqual(['value3', 'value1', 'value2'])
67
+ })
68
+ })
@@ -0,0 +1,5 @@
1
+ export { default } from './Filters'
2
+
3
+ export { filterOrderOptions, filterStyleOptions, useFilters } from './Filters'
4
+
5
+ export { handleSorting } from './helpers/handleSorting'
@@ -1,5 +1,3 @@
1
- @import '@cdc/core/styles/v2/themes/_color-definitions.scss';
2
-
3
1
  .cdc-editor .configure .type-dashboard .sidebar {
4
2
  top: 0;
5
3
  }
@@ -210,7 +208,7 @@
210
208
 
211
209
  svg {
212
210
  width: 60px;
213
- color: $blue;
211
+ color: var(--blue);
214
212
  margin-right: 1rem;
215
213
  height: 60px; // IE11
216
214
  path {
@@ -544,10 +542,6 @@
544
542
  font-weight: normal;
545
543
  }
546
544
 
547
- .btn {
548
- margin-top: 1em;
549
- }
550
-
551
545
  .sort-list {
552
546
  list-style: none;
553
547
 
@@ -1,6 +1,6 @@
1
1
  .cdc-open-viz-module {
2
2
  .cdc-chart-inner-container .cove-component__content {
3
- padding: 25px 15px !important;
3
+ padding: 25px 0px !important;
4
4
  }
5
5
  &.isEditor {
6
6
  overflow: auto;
@@ -1,10 +1,14 @@
1
1
  import { Group } from '@visx/group'
2
2
  import { Text } from '@visx/text'
3
- import { type ViewportSize, type MapConfig } from '@cdc/map/src/types/MapConfig'
3
+ import { type MapConfig } from '@cdc/map/src/types/MapConfig'
4
4
  import { type ChartConfig } from '@cdc/chart/src/types/ChartConfig'
5
- import { getGradientLegendWidth } from '@cdc/core/helpers/getGradientLegendWidth'
5
+ import { getTextWidth } from '../../helpers/getTextWidth'
6
6
  import { DimensionsType } from '../../types/Dimensions'
7
7
 
8
+ const MARGIN = 1
9
+ const BORDER_SIZE = 1
10
+ const MOBILE_BREAKPOINT = 576
11
+
8
12
  type CombinedConfig = MapConfig | ChartConfig
9
13
 
10
14
  interface GradientProps {
@@ -12,8 +16,7 @@ interface GradientProps {
12
16
  colors: string[]
13
17
  config: CombinedConfig
14
18
  dimensions: DimensionsType
15
- currentViewport: ViewportSize
16
- getTextWidth: (text: string, font: string) => string
19
+ parentPaddingToSubtract?: number
17
20
  }
18
21
 
19
22
  const LegendGradient = ({
@@ -21,29 +24,32 @@ const LegendGradient = ({
21
24
  colors,
22
25
  config,
23
26
  dimensions,
24
- currentViewport,
25
- getTextWidth
27
+ parentPaddingToSubtract = 0
26
28
  }: GradientProps): JSX.Element => {
29
+ const { uid, legend, type } = config
30
+ const { tickRotation, position, style, subStyle, hideBorder } = legend
31
+
32
+ const isLinearBlocks = subStyle === 'linear blocks'
27
33
  let [width] = dimensions
28
34
 
29
- const legendWidth = getGradientLegendWidth(width, currentViewport)
30
- const uniqueID = `${config.uid}-${Date.now()}`
35
+ const smallScreen = width <= MOBILE_BREAKPOINT
36
+ const legendWidth = Number(width) - parentPaddingToSubtract - MARGIN * 2 - BORDER_SIZE * 2
37
+ const uniqueID = `${uid}-${Date.now()}`
31
38
 
32
39
  const numTicks = colors?.length
33
40
 
34
41
  const longestLabel = labels && labels.length > 0 ? labels.reduce((a, b) => (a.length > b.length ? a : b)) : ''
35
42
  const boxHeight = 20
36
43
  let height = 50
37
- const margin = 1
38
44
 
39
45
  // configure tick witch and angle
40
46
  const textWidth = getTextWidth(longestLabel, `normal 14px sans-serif`)
41
- const rotationAngle = Number(config.legend.tickRotation) || 0
47
+ const rotationAngle = Number(tickRotation) || 0
42
48
  // Convert the angle from degrees to radians
43
49
  const angleInRadians = rotationAngle * (Math.PI / 180)
44
50
  const newHeight = height + Number(textWidth) * Math.sin(angleInRadians)
45
51
 
46
- // configre gradient colors
52
+ // configure gradient colors
47
53
  const stops = colors.map((color, index) => {
48
54
  const offset = (index / (colors.length - 1)) * 100
49
55
  return <stop key={index} offset={`${offset}%`} style={{ stopColor: color, stopOpacity: 1 }} />
@@ -52,69 +58,71 @@ const LegendGradient = ({
52
58
  // render ticks and labels
53
59
  const ticks = labels.map((key, index) => {
54
60
  const segmentWidth = legendWidth / numTicks
55
- const xPositionX = index * segmentWidth + segmentWidth
61
+ const xPositionX = index * segmentWidth + segmentWidth + MARGIN
56
62
  const textAnchor = rotationAngle ? 'end' : 'middle'
57
63
  const verticalAnchor = rotationAngle ? 'middle' : 'start'
64
+ const lastTick = index === labels.length - 1
58
65
 
59
66
  return (
60
- <Group top={margin}>
61
- <line x1={xPositionX} x2={xPositionX} y1={30} y2={boxHeight} stroke='black' />
67
+ <Group top={MARGIN}>
68
+ {!lastTick && !isLinearBlocks && <line x1={xPositionX} x2={xPositionX} y1={30} y2={boxHeight} stroke='black' />}
62
69
  <Text
63
- angle={-config.legend.tickRotation}
70
+ angle={-tickRotation}
64
71
  x={xPositionX}
65
72
  y={boxHeight}
66
73
  dy={10}
67
74
  dx={-segmentWidth / 2}
68
- fontSize='14'
75
+ fontSize={smallScreen ? '12' : '14'}
69
76
  textAnchor={textAnchor}
70
77
  verticalAnchor={verticalAnchor}
78
+ width={segmentWidth}
79
+ lineHeight={'14'}
71
80
  >
72
81
  {key}
73
82
  </Text>
74
83
  </Group>
75
84
  )
76
85
  })
77
- if ((config.type === 'map' && config.legend.position === 'side') || !config.legend.position) {
86
+ if ((type === 'map' && position === 'side') || !position) {
78
87
  return
79
88
  }
80
- if (
81
- config.type === 'chart' &&
82
- (config.legend.position === 'left' || config.legend.position === 'right' || !config.legend.position)
83
- ) {
89
+ if (type === 'chart' && (position === 'left' || position === 'right' || !position)) {
84
90
  return
85
91
  }
86
92
 
87
- if (config.legend.style === 'gradient') {
93
+ if (style === 'gradient') {
88
94
  return (
89
- <svg style={{ overflow: 'visible', width: '100%', marginTop: 10 }} height={newHeight}>
95
+ <svg
96
+ style={{ overflow: 'visible', width: '100%', marginTop: 10, marginBottom: hideBorder ? 10 : 0 }}
97
+ height={newHeight}
98
+ >
90
99
  {/* background border*/}
91
- <rect
92
- x={0}
93
- y={0}
94
- width={legendWidth + margin * 2}
95
- height={boxHeight + margin * 2}
96
- fill='#d3d3d3'
97
- strokeWidth='0.5'
98
- />
100
+ <rect x={0} y={0} width={legendWidth + MARGIN * 2} height={boxHeight + MARGIN * 2} fill='#d3d3d3' />
99
101
  {/* Define the gradient */}
100
102
  <linearGradient id={`gradient-smooth-${uniqueID}`} x1='0%' y1='0%' x2='100%' y2='0%'>
101
103
  {stops}
102
104
  </linearGradient>
103
105
 
104
- {config.legend.subStyle === 'smooth' && (
105
- <rect x={1} y={1} width={legendWidth} height={boxHeight} fill={`url(#gradient-smooth-${uniqueID})`} />
106
+ {subStyle === 'smooth' && (
107
+ <rect
108
+ x={MARGIN}
109
+ y={MARGIN}
110
+ width={legendWidth}
111
+ height={boxHeight}
112
+ fill={`url(#gradient-smooth-${uniqueID})`}
113
+ />
106
114
  )}
107
115
 
108
- {config.legend.subStyle === 'linear blocks' &&
116
+ {subStyle === 'linear blocks' &&
109
117
  colors.map((color, index) => {
110
118
  const segmentWidth = legendWidth / numTicks
111
- const xPosition = index * segmentWidth
119
+ const xPosition = index * segmentWidth + MARGIN
112
120
  return (
113
121
  <Group>
114
122
  <rect
115
123
  key={index}
116
124
  x={xPosition}
117
- y={0}
125
+ y={MARGIN}
118
126
  width={segmentWidth}
119
127
  height={boxHeight}
120
128
  fill={color}
@@ -0,0 +1,33 @@
1
+ import React, { useEffect, useRef } from 'react'
2
+ import './loader.styles.css'
3
+
4
+ type LoaderProps = {
5
+ fullScreen?: boolean
6
+ }
7
+
8
+ const Spinner = () => (
9
+ <div className='spinner-border text-primary' role='status'>
10
+ <span className='sr-only'>Loading...</span>
11
+ </div>
12
+ )
13
+
14
+ const Loader: React.FC<LoaderProps> = ({ fullScreen = false }) => {
15
+ const backgroundRef = useRef(null)
16
+
17
+ useEffect(() => {
18
+ if (backgroundRef?.current) {
19
+ const backgroundHeight = backgroundRef.current.parentElement.clientHeight
20
+ backgroundRef.current.style.height = `${backgroundHeight}px`
21
+ }
22
+ }, [])
23
+
24
+ return fullScreen ? (
25
+ <div ref={backgroundRef} className='cove-loader fullscreen'>
26
+ <Spinner />
27
+ </div>
28
+ ) : (
29
+ <Spinner />
30
+ )
31
+ }
32
+
33
+ export default Loader
@@ -0,0 +1 @@
1
+ export { default } from './Loader'
@@ -0,0 +1,13 @@
1
+ .cove-loader {
2
+ &.fullscreen {
3
+ background: rgba(255, 255, 255, 0.8);
4
+ position: absolute;
5
+ width: 100%;
6
+ display: flex;
7
+ justify-content: center;
8
+ z-index: 100;
9
+ & > * {
10
+ margin-top: 40vh;
11
+ }
12
+ }
13
+ }
@@ -22,7 +22,17 @@ interface MultiSelectProps {
22
22
  tooltip?: React.ReactNode
23
23
  }
24
24
 
25
- const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection = null, fieldName, label, options, updateField, selected = [], limit, tooltip }) => {
25
+ const MultiSelect: React.FC<MultiSelectProps> = ({
26
+ section = null,
27
+ subsection = null,
28
+ fieldName,
29
+ label,
30
+ options,
31
+ updateField,
32
+ selected = [],
33
+ limit,
34
+ tooltip
35
+ }) => {
26
36
  const preselectedItems = options.filter(opt => selected.includes(opt.value)).slice(0, limit)
27
37
  const [selectedItems, setSelectedItems] = useState<Option[]>(preselectedItems)
28
38
  const [expanded, setExpanded] = useState(false)
@@ -67,77 +77,90 @@ const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection =
67
77
 
68
78
  const multiID = 'multiSelect_' + label
69
79
  return (
70
- <div ref={multiSelectRef} className='cove-multiselect'>
80
+ <>
71
81
  {label && (
72
- <span id={multiID} className='edit-label column-heading'>
82
+ <label className='text-capitalize font-weight-bold' id={multiID + label} htmlFor={multiID}>
73
83
  {label}
74
- </span>
84
+ </label>
75
85
  )}
86
+ <div ref={multiSelectRef} className='cove-multiselect'>
87
+ {tooltip && tooltip}
76
88
 
77
- {tooltip && tooltip}
78
-
79
- <div className='wrapper'>
80
- <div className='selected'>
81
- {selectedItems.map(item => (
82
- <div key={item.value} aria-labelledby={label ? multiID : undefined}>
83
- {item.label}
84
- <button
85
- aria-label='Remove'
86
- onClick={e => {
87
- e.preventDefault()
88
- handleItemRemove(item)
89
- }}
90
- onKeyUp={e => {
91
- handleItemRemove(item, e)
92
- }}
93
- >
94
- x
95
- </button>
96
- </div>
97
- ))}
98
- <button
99
- aria-label={expanded ? 'Collapse' : 'Expand'}
100
- aria-labelledby={label ? multiID : undefined}
101
- className='expand'
102
- onClick={e => {
103
- e.preventDefault()
104
- setExpanded(!expanded)
89
+ <div className='wrapper'>
90
+ <div
91
+ id={multiID}
92
+ onClick={() => {
93
+ if (!selectedItems.length) {
94
+ setExpanded(true)
95
+ }
105
96
  }}
97
+ className='selected'
106
98
  >
107
- <Icon display={expanded ? 'caretDown' : 'caretUp'} style={{ cursor: 'pointer' }} />
108
- </button>
109
- </div>
110
- {!!limit && (
111
- <Tooltip style={{ textTransform: 'none' }}>
112
- <Tooltip.Target>
113
- <Icon display='question' style={{ marginLeft: '0.5rem' }} />
114
- </Tooltip.Target>
115
- <Tooltip.Content>
116
- <p>Select up to {limit} items</p>
117
- </Tooltip.Content>
118
- </Tooltip>
119
- )}
120
- </div>
121
- <ul className={'dropdown' + (expanded ? '' : ' d-none')}>
122
- {options
123
- .filter(option => !selectedItems.find(item => item.value === option.value))
124
- .map(option => (
125
- <li
126
- className='cove-multiselect-li'
127
- key={option.value}
128
- role='option'
129
- tabIndex={0}
99
+ {selectedItems.length ? (
100
+ selectedItems.map(item => (
101
+ <div key={item.value} aria-labelledby={label ? multiID + label : undefined}>
102
+ {item.label}
103
+ <button
104
+ aria-label='Remove'
105
+ onClick={e => {
106
+ e.preventDefault()
107
+ handleItemRemove(item)
108
+ }}
109
+ onKeyUp={e => {
110
+ handleItemRemove(item, e)
111
+ }}
112
+ >
113
+ x
114
+ </button>
115
+ </div>
116
+ ))
117
+ ) : (
118
+ <span className='pl-1 pt-1'>- Select -</span>
119
+ )}
120
+ <button
121
+ aria-label={expanded ? 'Collapse' : 'Expand'}
122
+ aria-labelledby={label ? multiID : undefined}
123
+ className='expand'
130
124
  onClick={e => {
131
125
  e.preventDefault()
132
- handleItemSelect(option, e)
126
+ setExpanded(!expanded)
133
127
  }}
134
- onKeyUp={e => handleItemSelect(option, e)}
135
128
  >
136
- {option.label}
137
- </li>
138
- ))}
139
- </ul>
140
- </div>
129
+ <Icon display={'caretDown'} style={{ cursor: 'pointer' }} />
130
+ </button>
131
+ </div>
132
+ {!!limit && (
133
+ <Tooltip style={{ textTransform: 'none' }}>
134
+ <Tooltip.Target>
135
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
136
+ </Tooltip.Target>
137
+ <Tooltip.Content>
138
+ <p>Select up to {limit} items</p>
139
+ </Tooltip.Content>
140
+ </Tooltip>
141
+ )}
142
+ </div>
143
+ <ul className={'dropdown' + (expanded ? '' : ' d-none')}>
144
+ {options
145
+ .filter(option => !selectedItems.find(item => item.value === option.value))
146
+ .map(option => (
147
+ <li
148
+ className='cove-multiselect-li'
149
+ key={option.value}
150
+ role='option'
151
+ tabIndex={0}
152
+ onClick={e => {
153
+ e.preventDefault()
154
+ handleItemSelect(option, e)
155
+ }}
156
+ onKeyUp={e => handleItemSelect(option, e)}
157
+ >
158
+ {option.label}
159
+ </li>
160
+ ))}
161
+ </ul>
162
+ </div>
163
+ </>
141
164
  )
142
165
  }
143
166
 
@@ -1,5 +1,4 @@
1
1
  .cove-multiselect {
2
- position: relative;
3
2
  .cove-input__label {
4
3
  display: block;
5
4
  }
@@ -8,8 +7,7 @@
8
7
  align-items: center;
9
8
  .selected {
10
9
  border: 1px solid var(--lightGray);
11
- padding: 5px;
12
- min-height: 40px;
10
+ padding: 7px;
13
11
  min-width: 200px;
14
12
  display: inline-block;
15
13
  :is(button) {
@@ -25,10 +23,14 @@
25
23
  border-radius: 5px;
26
24
  }
27
25
  .expand {
28
- padding: 0 5px;
29
- border-radius: 5px;
30
- background: var(--lightGray);
26
+ padding: 2px 0px;
27
+ margin-right: -6px;
31
28
  float: right;
29
+ margin-bottom: -3px;
30
+ color: var(--mediumGray);
31
+ &:focus {
32
+ outline: none;
33
+ }
32
34
  }
33
35
  border-radius: 5px;
34
36
  }
@@ -43,7 +45,8 @@
43
45
  .dropdown {
44
46
  background: white;
45
47
  position: absolute;
46
- margin-top: 5px;
48
+ top: var(--select-height);
49
+ margin-top: 0px;
47
50
  border: 1px solid var(--lightGray);
48
51
  padding-left: 0;
49
52
  min-height: 40px;