@cdc/core 4.24.1 → 4.24.3

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 (71) hide show
  1. package/assets/icon-sankey.svg +1 -0
  2. package/assets/icon-table.svg +1 -0
  3. package/components/DataTable/DataTable.tsx +44 -15
  4. package/components/DataTable/DataTableStandAlone.tsx +15 -0
  5. package/components/DataTable/components/CellAnchor.tsx +3 -1
  6. package/components/DataTable/components/ChartHeader.tsx +48 -12
  7. package/components/DataTable/components/DataTableEditorPanel.tsx +42 -0
  8. package/components/DataTable/components/MapHeader.tsx +10 -5
  9. package/components/DataTable/helpers/customColumns.ts +4 -2
  10. package/components/DataTable/helpers/customSort.ts +9 -0
  11. package/components/DataTable/helpers/getChartCellValue.ts +5 -3
  12. package/components/DataTable/helpers/getDataSeriesColumns.ts +10 -2
  13. package/components/DataTable/helpers/getSeriesName.ts +15 -20
  14. package/components/DataTable/helpers/mapCellMatrix.tsx +4 -0
  15. package/components/DataTable/types/TableConfig.ts +12 -37
  16. package/components/EditorPanel/ColumnsEditor.tsx +311 -0
  17. package/components/EditorPanel/DataTableEditor.tsx +27 -28
  18. package/components/Filters.jsx +35 -16
  19. package/components/MultiSelect/MultiSelect.tsx +39 -20
  20. package/components/MultiSelect/multiselect.styles.css +44 -27
  21. package/components/NestedDropdown/NestedDropdown.tsx +257 -0
  22. package/components/NestedDropdown/index.ts +1 -0
  23. package/components/NestedDropdown/nesteddropdown.styles.css +70 -0
  24. package/components/Table/Table.tsx +1 -1
  25. package/components/_stories/MultiSelect.stories.tsx +10 -1
  26. package/components/_stories/NestedDropdown.stories.tsx +58 -0
  27. package/components/createBarElement.jsx +117 -0
  28. package/components/elements/ScreenReaderText.tsx +8 -0
  29. package/components/elements/SkipTo.tsx +14 -0
  30. package/components/ui/Icon.tsx +5 -1
  31. package/components/ui/Title/Title.scss +7 -1
  32. package/components/ui/Title/index.tsx +3 -3
  33. package/components/ui/Tooltip.jsx +1 -1
  34. package/components/ui/_stories/Colors.stories.tsx +92 -0
  35. package/components/ui/_stories/Icon.stories.tsx +17 -10
  36. package/data/colorPalettes.js +1 -6
  37. package/helpers/cove/accessibility.ts +23 -0
  38. package/helpers/cove/date.ts +19 -0
  39. package/helpers/coveUpdateWorker.js +4 -0
  40. package/helpers/fetchRemoteData.js +5 -5
  41. package/helpers/getViewport.ts +23 -0
  42. package/helpers/isDomainExternal.js +14 -0
  43. package/helpers/isSolr.js +13 -0
  44. package/helpers/queryStringUtils.js +26 -0
  45. package/helpers/tests/updateFieldFactory.test.ts +89 -0
  46. package/helpers/updateFieldFactory.ts +38 -0
  47. package/helpers/useDataVizClasses.js +2 -2
  48. package/helpers/ver/4.24.3.js +25 -0
  49. package/helpers/withDevTools.ts +50 -0
  50. package/package.json +4 -3
  51. package/styles/_data-table.scss +2 -20
  52. package/styles/_global-variables.scss +75 -0
  53. package/styles/base.scss +97 -69
  54. package/types/Action.ts +1 -0
  55. package/types/Axis.ts +3 -0
  56. package/types/BaseVisualizationType.ts +1 -0
  57. package/types/BoxPlot.ts +21 -0
  58. package/types/Column.ts +1 -0
  59. package/types/ConfidenceInterval.ts +1 -0
  60. package/types/General.ts +9 -0
  61. package/types/Legend.ts +18 -0
  62. package/types/Region.ts +10 -0
  63. package/types/Runtime.ts +3 -1
  64. package/types/Table.ts +5 -2
  65. package/types/UpdateFieldFunc.ts +1 -1
  66. package/types/ViewPort.ts +2 -0
  67. package/types/Visualization.ts +23 -5
  68. package/types/WCMSProps.ts +11 -0
  69. package/components/DataTable/components/SkipNav.tsx +0 -7
  70. package/helpers/cove/date.js +0 -9
  71. package/helpers/getViewport.js +0 -21
@@ -0,0 +1,311 @@
1
+ import { AccordionItem, AccordionItemButton, AccordionItemHeading, AccordionItemPanel } from 'react-accessible-accordion'
2
+ import Tooltip from '../ui/Tooltip'
3
+ import Icon from '../ui/Icon'
4
+ import { TextField } from './Inputs'
5
+ import { Visualization } from '../../types/Visualization'
6
+ import { UpdateFieldFunc } from '../../types/UpdateFieldFunc'
7
+ import { Column } from '../../types/Column'
8
+ import _ from 'lodash'
9
+
10
+ interface ColumnsEditorProps {
11
+ config: Visualization
12
+ updateField: UpdateFieldFunc<string | boolean | string[] | number | Column>
13
+ deleteColumn: (colName: string) => void
14
+ }
15
+
16
+ const ColumnsEditor: React.FC<ColumnsEditorProps> = ({ config, updateField, deleteColumn }) => {
17
+ const additionalColumns = Object.keys(config.columns).filter(value => {
18
+ const dataKey = config.xAxis?.dataKey
19
+ const defaultCols = dataKey ? [dataKey] : []
20
+
21
+ if (true === defaultCols.includes(value)) {
22
+ return false
23
+ }
24
+ return true
25
+ })
26
+
27
+ const editColumn = (addCol, columnName, setval) => {
28
+ updateField('columns', addCol, columnName, setval)
29
+ }
30
+
31
+ // just adds a new column but not set to any data yet
32
+ const addAdditionalColumn = number => {
33
+ const columnKey = `additionalColumn${number}`
34
+ const newColumn: Column = {
35
+ label: 'New Column',
36
+ dataTable: false,
37
+ tooltips: false,
38
+ prefix: '',
39
+ suffix: '',
40
+ forestPlot: false,
41
+ startingPoint: '0',
42
+ forestPlotAlignRight: false,
43
+ roundToPlace: 0,
44
+ commas: false,
45
+ showInViz: false,
46
+ forestPlotStartingPoint: 0
47
+ }
48
+
49
+ updateField('columns', null, columnKey, newColumn)
50
+ }
51
+
52
+ const getColumns = () => {
53
+ const columns: string[] = config.data.flatMap(row => {
54
+ return Object.keys(row).map(columnName => columnName)
55
+ })
56
+
57
+ const { lower, upper } = config.confidenceKeys || {}
58
+ return _.uniq(columns).filter(key => {
59
+ const keyIsPresentInSeries = config.series?.filter(series => series.dataKey === key).length > 0
60
+ if (keyIsPresentInSeries || (config.confidenceKeys && Object.keys(config.confidenceKeys).includes(key) && (lower || upper) && key !== lower && key !== upper)) {
61
+ return false
62
+ }
63
+ return true
64
+ })
65
+ }
66
+
67
+ return (
68
+ <AccordionItem>
69
+ <AccordionItemHeading>
70
+ <AccordionItemButton>Columns</AccordionItemButton>
71
+ </AccordionItemHeading>
72
+ <AccordionItemPanel>
73
+ {'navigation' !== config.type && (
74
+ <fieldset className='primary-fieldset edit-block'>
75
+ <label>
76
+ <span className='edit-label'>
77
+ Configurations
78
+ <Tooltip style={{ textTransform: 'none' }}>
79
+ <Tooltip.Target>
80
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
81
+ </Tooltip.Target>
82
+ <Tooltip.Content>
83
+ <p>You can specify additional columns to display in tooltips and / or the supporting data table.</p>
84
+ </Tooltip.Content>
85
+ </Tooltip>
86
+ </span>
87
+ </label>
88
+ {additionalColumns.map(val => (
89
+ <fieldset className='edit-block' key={val}>
90
+ <button
91
+ className='remove-column'
92
+ onClick={event => {
93
+ event.preventDefault()
94
+ deleteColumn(val)
95
+ }}
96
+ >
97
+ Remove
98
+ </button>
99
+ <label>
100
+ <span className='edit-label column-heading'>Column</span>
101
+ <select
102
+ value={config.columns[val] ? config.columns[val].name : undefined}
103
+ onChange={event => {
104
+ editColumn(val, 'name', event.target.value)
105
+ }}
106
+ >
107
+ {['-Select-', ...getColumns()].map(option => (
108
+ <option>{option}</option>
109
+ ))}
110
+ </select>
111
+ </label>
112
+ {config.type !== 'table' && (
113
+ <label>
114
+ <span className='edit-label column-heading'>Associate to Series</span>
115
+ <select
116
+ value={config.columns[val] ? config.columns[val].series : ''}
117
+ onChange={event => {
118
+ editColumn(val, 'series', event.target.value)
119
+ }}
120
+ >
121
+ <option value=''>Select series</option>
122
+ {(config.series || []).map(series => (
123
+ <option>{series.dataKey}</option>
124
+ ))}
125
+ </select>
126
+ </label>
127
+ )}
128
+
129
+ <TextField value={config.columns[val].label} section='columns' subsection={val} fieldName='label' label='Label' updateField={updateField} />
130
+ <ul className='column-edit'>
131
+ <li className='three-col'>
132
+ <TextField value={config.columns[val].prefix} section='columns' subsection={val} fieldName='prefix' label='Prefix' updateField={updateField} />
133
+ <TextField value={config.columns[val].suffix} section='columns' subsection={val} fieldName='suffix' label='Suffix' updateField={updateField} />
134
+ <TextField type='number' value={config.columns[val].roundToPlace} section='columns' subsection={val} fieldName='roundToPlace' label='Round' updateField={updateField} />
135
+ </li>
136
+ <li>
137
+ <label className='checkbox'>
138
+ <input
139
+ type='checkbox'
140
+ checked={config.columns[val].commas}
141
+ onChange={event => {
142
+ editColumn(val, 'commas', event.target.checked)
143
+ }}
144
+ />
145
+ <span className='edit-label'>Add Commas to Numbers</span>
146
+ </label>
147
+ </li>
148
+ {config.type !== 'table' && (
149
+ <li>
150
+ {config.table.showVertical && (
151
+ <label className='checkbox'>
152
+ <input
153
+ type='checkbox'
154
+ checked={config.columns[val].dataTable}
155
+ onChange={event => {
156
+ editColumn(val, 'dataTable', event.target.checked)
157
+ }}
158
+ />
159
+ <span className='edit-label'>Show in Data Table</span>
160
+ </label>
161
+ )}
162
+ </li>
163
+ )}
164
+ {config.visualizationType === 'Pie' && (
165
+ <li>
166
+ <label className='checkbox'>
167
+ <input
168
+ type='checkbox'
169
+ checked={config.columns[val].showInViz}
170
+ onChange={event => {
171
+ editColumn(val, 'showInViz', event.target.checked)
172
+ }}
173
+ />
174
+ <span className='edit-label'>Show in Visualization</span>
175
+ </label>
176
+ </li>
177
+ )}
178
+ {config.type !== 'table' && (
179
+ <li>
180
+ <label className='checkbox'>
181
+ <input
182
+ type='checkbox'
183
+ checked={config.columns[val].tooltips || false}
184
+ onChange={event => {
185
+ updateField('columns', val, 'tooltips', event.target.checked)
186
+ }}
187
+ />
188
+ <span className='edit-label'>Show in tooltip</span>
189
+ </label>
190
+ </li>
191
+ )}
192
+
193
+ {config.visualizationType === 'Forest Plot' && (
194
+ <>
195
+ <li>
196
+ <label className='checkbox'>
197
+ <input
198
+ type='checkbox'
199
+ checked={config.columns[val].forestPlot || false}
200
+ onChange={event => {
201
+ editColumn(val, 'forestPlot', event.target.checked)
202
+ }}
203
+ />
204
+ <span className='edit-label'>Show in Forest Plot</span>
205
+ </label>
206
+ </li>
207
+ <li>
208
+ <label className='checkbox'>
209
+ <input
210
+ type='checkbox'
211
+ checked={config.columns[val].forestPlotAlignRight || false}
212
+ onChange={event => {
213
+ editColumn(val, 'forestPlotAlignRight', event.target.checked)
214
+ }}
215
+ />
216
+ <span className='edit-label'>Align Right</span>
217
+ </label>
218
+ </li>
219
+
220
+ {!config.columns[val].forestPlotAlignRight && (
221
+ <li>
222
+ <label className='text'>
223
+ <span className='edit-label'>Forest Plot Starting Point</span>
224
+ <input
225
+ type='number'
226
+ value={config.columns[val].forestPlotStartingPoint || 0}
227
+ onChange={event => {
228
+ editColumn(val, 'forestPlotStartingPoint', event.target.value)
229
+ }}
230
+ />
231
+ </label>
232
+ </li>
233
+ )}
234
+ </>
235
+ )}
236
+ </ul>
237
+ </fieldset>
238
+ ))}
239
+ <button
240
+ className={'btn full-width'}
241
+ onClick={event => {
242
+ event.preventDefault()
243
+ addAdditionalColumn(additionalColumns.length + 1)
244
+ }}
245
+ >
246
+ Add Column Configuration
247
+ </button>
248
+ </fieldset>
249
+ )}
250
+ {'category' === config.legend?.type && (
251
+ <fieldset className='primary-fieldset edit-block'>
252
+ <label>
253
+ <span className='edit-label'>
254
+ Additional Category
255
+ <Tooltip style={{ textTransform: 'none' }}>
256
+ <Tooltip.Target>
257
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
258
+ </Tooltip.Target>
259
+ <Tooltip.Content>
260
+ <p>You can provide additional categories to ensure they appear in the legend</p>
261
+ </Tooltip.Content>
262
+ </Tooltip>
263
+ </span>
264
+ </label>
265
+ {config.legend.additionalCategories &&
266
+ config.legend.additionalCategories.map((val, i) => (
267
+ <fieldset className='edit-block' key={val}>
268
+ <button
269
+ className='remove-column'
270
+ onClick={event => {
271
+ event.preventDefault()
272
+ const updatedAdditionaCategories = [...config.legend.additionalCategories]
273
+ updatedAdditionaCategories.splice(i, 1)
274
+ updateField('legend', null, 'additionalCategories', updatedAdditionaCategories)
275
+ }}
276
+ >
277
+ Remove
278
+ </button>
279
+ <TextField
280
+ value={val}
281
+ label='Category'
282
+ section='legend'
283
+ subsection={null}
284
+ fieldName='additionalCategories'
285
+ updateField={(section, subsection, fieldName, value) => {
286
+ const updatedAdditionaCategories = [...config.legend.additionalCategories]
287
+ updatedAdditionaCategories[i] = value
288
+ updateField(section, subsection, fieldName, updatedAdditionaCategories)
289
+ }}
290
+ />
291
+ </fieldset>
292
+ ))}
293
+ <button
294
+ className={'btn full-width'}
295
+ onClick={event => {
296
+ event.preventDefault()
297
+ const updatedAdditionaCategories = [...(config.legend.additionalCategories || [])]
298
+ updatedAdditionaCategories.push('')
299
+ updateField('legend', null, 'additionalCategories', updatedAdditionaCategories)
300
+ }}
301
+ >
302
+ Add Category
303
+ </button>
304
+ </fieldset>
305
+ )}
306
+ </AccordionItemPanel>
307
+ </AccordionItem>
308
+ )
309
+ }
310
+
311
+ export default ColumnsEditor
@@ -5,26 +5,24 @@ import { CheckBox, TextField } from './Inputs'
5
5
  import type { Table } from '@cdc/core/types/Table'
6
6
  import MultiSelect from '../MultiSelect'
7
7
  import { UpdateFieldFunc } from '../../types/UpdateFieldFunc'
8
+ import { Visualization } from '../../types/Visualization'
8
9
 
9
10
  interface DataTableProps {
10
- config: {
11
- table: Table
12
- visualizationType: string
13
- }
11
+ config: Visualization
14
12
  updateField: UpdateFieldFunc<string | boolean | string[] | number>
15
13
  isDashboard: boolean
16
- isLoadedFromUrl: boolean
17
14
  columns: string[]
18
15
  }
19
16
 
20
- const DataTable: React.FC<DataTableProps> = ({ config, updateField, isDashboard, isLoadedFromUrl, columns }) => {
17
+ const DataTable: React.FC<DataTableProps> = ({ config, updateField, isDashboard, columns }) => {
18
+ const isLoadedFromUrl = config.dataKey?.includes('http://') || config?.dataKey?.includes('https://')
21
19
  return (
22
20
  <>
23
21
  <TextField
24
22
  value={config.table.label}
25
23
  updateField={updateField}
26
24
  section='table'
27
- fieldName='table-label'
25
+ fieldName='label'
28
26
  id='tableLabel'
29
27
  label='Data Table Title'
30
28
  placeholder='Data Table'
@@ -98,30 +96,31 @@ const DataTable: React.FC<DataTableProps> = ({ config, updateField, isDashboard,
98
96
  }
99
97
  />
100
98
  <CheckBox value={config.table.limitHeight} section='table' fieldName='limitHeight' label='Limit Table Height' updateField={updateField} />
101
- <CheckBox
102
- value={config.table.customTableConfig}
103
- fieldName='customTableConfig'
104
- label='Customize Table Config'
105
- section='table'
106
- updateField={updateField}
107
- tooltip={
108
- <Tooltip style={{ textTransform: 'none' }}>
109
- <Tooltip.Target>
110
- <Icon display='question' style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }} />
111
- </Tooltip.Target>
112
- <Tooltip.Content>
113
- <p>This will display all available columns in the data set. It will not show any columns where all of the column cells are null.</p>
114
- </Tooltip.Content>
115
- </Tooltip>
116
- }
117
- />
99
+ {config.type !== 'table' && (
100
+ <CheckBox
101
+ value={config.table.customTableConfig}
102
+ fieldName='customTableConfig'
103
+ label='Customize Table Config'
104
+ section='table'
105
+ updateField={updateField}
106
+ tooltip={
107
+ <Tooltip style={{ textTransform: 'none' }}>
108
+ <Tooltip.Target>
109
+ <Icon display='question' style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }} />
110
+ </Tooltip.Target>
111
+ <Tooltip.Content>
112
+ <p>This will display all available columns in the data set. It will not show any columns where all of the column cells are null.</p>
113
+ </Tooltip.Content>
114
+ </Tooltip>
115
+ }
116
+ />
117
+ )}
118
118
  {config.table.customTableConfig && <MultiSelect options={columns.map(c => ({ label: c, value: c }))} fieldName='excludeColumns' label='Exclude Columns' section='table' updateField={updateField} />}
119
- {config.table.limitHeight && <TextField value={config.table.height} fieldName='height' label='Data Table Height' type='number' min={0} max={500} placeholder='Height(px)' updateField={updateField} />}
119
+ {config.table.limitHeight && <TextField value={config.table.height} section='table' fieldName='height' label='Data Table Height' type='number' min={0} max={500} placeholder='Height(px)' updateField={updateField} />}
120
120
  <CheckBox value={config.table.expanded} fieldName='expanded' label='Expanded by Default' section='table' updateField={updateField} />
121
- {isDashboard && <CheckBox value={config.table.showDataTableLink} fieldName='showDataTableLink' label='Show Data Table Name & Link' section='table' updateField={updateField} />}
121
+ {isDashboard && config.type !== 'table' && <CheckBox value={config.table.showDataTableLink} fieldName='showDataTableLink' label='Show Data Table Name & Link' section='table' updateField={updateField} />}
122
122
  {isLoadedFromUrl && <CheckBox value={config.table.showDownloadUrl} fieldName='showDownloadUrl' label='Show URL to Automatically Updated Data' section='table' updateField={updateField} />}
123
- <CheckBox value={config.table.download} fieldName='download' label='Show Download CSV Link' section='table' updateField={updateField} />
124
- <CheckBox value={config.table.showDownloadImgButton} fieldName='showDownloadImgButton' label='Display Image Button' section='table' updateField={updateField} />
123
+ {config.type !== 'table' && <CheckBox value={config.table.showDownloadImgButton} fieldName='showDownloadImgButton' label='Display Image Button' section='table' updateField={updateField} />}
125
124
  <label>
126
125
  <span className='edit-label column-heading'>Table Cell Min Width</span>
127
126
  <input type='number' value={config.table.cellMinWidth ? config.table.cellMinWidth : 0} onChange={e => updateField('table', null, 'cellMinWidth', e.target.value)} />
@@ -3,6 +3,7 @@ import { useId } from 'react'
3
3
 
4
4
  // CDC
5
5
  import Button from '@cdc/core/components/elements/Button'
6
+ import { getQueryParams, updateQueryString } from '@cdc/core/helpers/queryStringUtils'
6
7
 
7
8
  // Third Party
8
9
  import PropTypes from 'prop-types'
@@ -72,24 +73,26 @@ export const useFilters = props => {
72
73
  const announceChange = text => {}
73
74
 
74
75
  const changeFilterActive = (index, value) => {
75
- let newFilters = visualizationConfig.type === 'map' ? [...filteredData] : [...visualizationConfig.filters]
76
+ const newFilters = visualizationConfig.type === 'map' ? [...filteredData] : [...visualizationConfig.filters]
76
77
 
77
- newFilters[index].active = value
78
- setConfig({ ...visualizationConfig })
79
-
80
- // If this is a button filter type show the button.
81
78
  if (visualizationConfig.filterBehavior === 'Apply Button') {
79
+ newFilters[index].queuedActive = value
82
80
  setShowApplyButton(true)
81
+ } else {
82
+ const newFilter = newFilters[index]
83
+ newFilter.active = value
84
+
85
+ const queryParams = getQueryParams()
86
+ if (newFilter.setByQueryParameter && queryParams[newFilter.setByQueryParameter] !== newFilter.active) {
87
+ queryParams[newFilter.setByQueryParameter] = newFilter.active
88
+ updateQueryString(queryParams)
89
+ }
83
90
  }
91
+ setConfig({
92
+ ...visualizationConfig,
93
+ filters: newFilters
94
+ })
84
95
 
85
- // If we're not using the apply button we can set the filters right away.
86
- if (visualizationConfig.filterBehavior !== 'Apply Button') {
87
- setConfig({
88
- ...visualizationConfig,
89
- filters: newFilters
90
- })
91
- }
92
-
93
96
  // Used for setting active filter, fromHash breaks the filteredData functionality.
94
97
  if (visualizationConfig.type === 'map' && visualizationConfig.filterBehavior === 'Filter Change') {
95
98
  setFilteredData(newFilters)
@@ -102,6 +105,22 @@ export const useFilters = props => {
102
105
  }
103
106
 
104
107
  const handleApplyButton = newFilters => {
108
+ let needsQueryUpdate = false
109
+ const queryParams = getQueryParams()
110
+ newFilters.forEach(newFilter => {
111
+ if (newFilter.queuedActive) {
112
+ newFilter.active = newFilter.queuedActive
113
+ delete newFilter.queuedActive
114
+ if (newFilter.setByQueryParameter && queryParams[newFilter.setByQueryParameter] !== newFilter.active) {
115
+ queryParams[newFilter.setByQueryParameter] = newFilter.active
116
+ needsQueryUpdate = true
117
+ }
118
+ }
119
+ })
120
+ if (needsQueryUpdate) {
121
+ updateQueryString(queryParams)
122
+ }
123
+
105
124
  setConfig({ ...visualizationConfig, filters: newFilters })
106
125
 
107
126
  if (type === 'map') {
@@ -219,7 +238,7 @@ const Filters = props => {
219
238
 
220
239
  const Filters = props => props.children
221
240
 
222
- const filterSectionClassList = ['filters-section', type === 'map' ? general.headerColor : theme]
241
+ const filterSectionClassList = ['filters-section', type === 'map' ? general.headerColor : visualizationConfig?.visualizationType === 'Spark Line' ? null : theme]
223
242
 
224
243
  // Exterior Section Wrapper
225
244
  Filters.Section = props => {
@@ -289,7 +308,7 @@ const Filters = props => {
289
308
  <select
290
309
  id={`filter-${outerIndex}`}
291
310
  name={label}
292
- aria-label={label}
311
+ aria-label={`Filter by ${label}`}
293
312
  className='filter-select'
294
313
  data-index='0'
295
314
  value={active}
@@ -351,7 +370,7 @@ const Filters = props => {
351
370
  )
352
371
 
353
372
  values.push(
354
- <option key={index} value={filterOption}>
373
+ <option key={index} value={filterOption} aria-label={filterOption}>
355
374
  {singleFilter.labels && singleFilter.labels[filterOption] ? singleFilter.labels[filterOption] : filterOption}
356
375
  </option>
357
376
  )
@@ -1,4 +1,5 @@
1
1
  import React, { useEffect, useRef, useState } from 'react'
2
+ import Tooltip from '../ui/Tooltip'
2
3
  import Icon from '../ui/Icon'
3
4
 
4
5
  import './multiselect.styles.css'
@@ -12,14 +13,17 @@ interface Option {
12
13
  interface MultiSelectProps {
13
14
  section?: string
14
15
  subsection?: string
15
- fieldName: string
16
+ fieldName: string | number
16
17
  options: Option[]
17
18
  updateField: UpdateFieldFunc<string[]>
18
19
  label?: string
20
+ selected?: string[]
21
+ limit?: number
19
22
  }
20
23
 
21
- const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection = null, fieldName, label, options, updateField }) => {
22
- const [selectedItems, setSelectedItems] = useState<Option[]>([])
24
+ const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection = null, fieldName, label, options, updateField, selected, limit }) => {
25
+ const preselectedItems = options.filter(opt => selected?.includes(opt.value)).slice(0, limit)
26
+ const [selectedItems, setSelectedItems] = useState<Option[]>(preselectedItems)
23
27
  const [expanded, setExpanded] = useState(false)
24
28
  const multiSelectRef = useRef(null)
25
29
 
@@ -45,13 +49,16 @@ const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection =
45
49
  newItems.map(item => item.value)
46
50
  )
47
51
 
48
- const handleItemSelect = (option: Option) => {
52
+ const handleItemSelect = (option: Option, e = null) => {
53
+ if (e && e.type === 'keyup' && e.key !== 'Enter') return
54
+ if (limit && selectedItems.length >= limit) return
49
55
  const newItems = [...selectedItems, option]
50
56
  setSelectedItems(newItems)
51
57
  update(newItems)
52
58
  }
53
59
 
54
- const handleItemRemove = (option: Option, caller: string) => {
60
+ const handleItemRemove = (option: Option, e = null) => {
61
+ if (e && e.type === 'keyup' && e.key !== 'Enter') return
55
62
  const newItems = selectedItems.filter(item => item.value !== option.value)
56
63
  setSelectedItems(newItems)
57
64
  update(newItems)
@@ -61,29 +68,41 @@ const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection =
61
68
  return (
62
69
  <div ref={multiSelectRef} className='cove-multiselect'>
63
70
  {label && (
64
- <span id={multiID} className='edit-label cove-input__label'>
71
+ <label id={multiID} className='cove-input__label'>
65
72
  {label}
66
- </span>
73
+ </label>
67
74
  )}
68
75
 
69
- <div aria-labelledby={label ? multiID : undefined} className='selected'>
70
- {selectedItems.map(item => (
71
- <div key={item.value} role='button' tabIndex={0} onClick={() => handleItemRemove(item, 'button click')} onKeyUp={() => handleItemRemove(item, 'button key up')}>
72
- {item.label}
73
- <button aria-label='Remove' onClick={() => handleItemRemove(item, 'X')}>
74
- x
75
- </button>
76
- </div>
77
- ))}
78
- <button aria-label={expanded ? 'Collapse' : 'Expand'} className='expand' onClick={() => setExpanded(!expanded)}>
79
- <Icon display={expanded ? 'caretDown' : 'caretUp'} style={{ cursor: 'pointer' }} />
80
- </button>
76
+ <div className='wrapper'>
77
+ <div className='selected'>
78
+ {selectedItems.map(item => (
79
+ <div key={item.value} aria-labelledby={label ? multiID : undefined} role='button' onClick={() => handleItemRemove(item)} onKeyUp={e => handleItemRemove(item, e)}>
80
+ {item.label}
81
+ <button aria-label='Remove' onClick={() => handleItemRemove(item)}>
82
+ x
83
+ </button>
84
+ </div>
85
+ ))}
86
+ <button aria-label={expanded ? 'Collapse' : 'Expand'} aria-labelledby={label ? multiID : undefined} className='expand' onClick={() => setExpanded(!expanded)}>
87
+ <Icon display={expanded ? 'caretDown' : 'caretUp'} style={{ cursor: 'pointer' }} />
88
+ </button>
89
+ </div>
90
+ {!!limit && (
91
+ <Tooltip style={{ textTransform: 'none' }}>
92
+ <Tooltip.Target>
93
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
94
+ </Tooltip.Target>
95
+ <Tooltip.Content>
96
+ <p>Select up to {limit} items</p>
97
+ </Tooltip.Content>
98
+ </Tooltip>
99
+ )}
81
100
  </div>
82
101
  <ul className={'dropdown' + (expanded ? '' : ' hide')}>
83
102
  {options
84
103
  .filter(option => !selectedItems.find(item => item.value === option.value))
85
104
  .map(option => (
86
- <li className='cove-multiselect-li' key={option.value} role='option' tabIndex={0} onClick={() => handleItemSelect(option)} onKeyUp={() => handleItemSelect(option)}>
105
+ <li className='cove-multiselect-li' key={option.value} role='option' tabIndex={0} onClick={() => handleItemSelect(option)} onKeyUp={e => handleItemSelect(option, e)}>
87
106
  {option.label}
88
107
  </li>
89
108
  ))}