@cdc/core 4.25.11 → 4.26.2

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 (147) hide show
  1. package/.claude/agents/qa-test-developer.md +126 -0
  2. package/CLAUDE.local.md +67 -0
  3. package/_stories/Gallery.Charts.stories.tsx +300 -0
  4. package/_stories/Gallery.DataBite.stories.tsx +79 -0
  5. package/_stories/Gallery.Maps.stories.tsx +239 -0
  6. package/_stories/Gallery.WaffleChart.stories.tsx +187 -0
  7. package/_stories/PageART.stories.tsx +193 -0
  8. package/_stories/PageBRFSS.stories.tsx +294 -0
  9. package/_stories/PageCancerRegistries.stories.tsx +199 -0
  10. package/_stories/PageEasternEquineEncephalitis.stories.tsx +216 -0
  11. package/_stories/PageExcessiveAlcoholUse.stories.tsx +201 -0
  12. package/_stories/PageMaternalMortality.stories.tsx +193 -0
  13. package/_stories/PageOralHealth.stories.tsx +201 -0
  14. package/_stories/PageRespiratory.stories.tsx +332 -0
  15. package/_stories/PageSmokingTobacco.stories.tsx +200 -0
  16. package/_stories/PageStateDiabetesProfiles.stories.tsx +201 -0
  17. package/_stories/PageWastewater.stories.tsx +477 -0
  18. package/_stories/VegaImport.stories.tsx +401 -0
  19. package/_stories/vega-fixtures/bars-with-line.json +444 -0
  20. package/_stories/vega-fixtures/bars.json +58 -0
  21. package/_stories/vega-fixtures/combo-bar-rolling-mean.json +88 -0
  22. package/_stories/vega-fixtures/combo.json +68 -0
  23. package/_stories/vega-fixtures/grouped-horizontal-bars.json +83 -0
  24. package/_stories/vega-fixtures/grouped-horizontal-bars2.json +231 -0
  25. package/_stories/vega-fixtures/horizontal-bar.json +427 -0
  26. package/_stories/vega-fixtures/horizontal-bars-with-bad-colors.json +197 -0
  27. package/_stories/vega-fixtures/horizontal-bars2.json +58 -0
  28. package/_stories/vega-fixtures/lines.json +227 -0
  29. package/_stories/vega-fixtures/measles-bars.json +348 -0
  30. package/_stories/vega-fixtures/measles-map.json +11101 -0
  31. package/_stories/vega-fixtures/measles-stacked-bars.json +2147 -0
  32. package/_stories/vega-fixtures/multi-dataset.json +255 -0
  33. package/_stories/vega-fixtures/no-data.json +14 -0
  34. package/_stories/vega-fixtures/pie-chart.json +94 -0
  35. package/_stories/vega-fixtures/repeat-spec.json +47 -0
  36. package/_stories/vega-fixtures/stacked-area.json +222 -0
  37. package/_stories/vega-fixtures/stacked-bar-with-rect.json +3412 -0
  38. package/_stories/vega-fixtures/stacked-bars-with-line.json +364 -0
  39. package/_stories/vega-fixtures/stacked-bars.json +212 -0
  40. package/_stories/vega-fixtures/stacked-horizontal-bars.json +140 -0
  41. package/_stories/vega-fixtures/warning-combo.json +59 -0
  42. package/_stories/vega-fixtures/warning-scatter-and-line.json +1182 -0
  43. package/assets/icon-chart-area.svg +1 -0
  44. package/assets/icon-chart-radar.svg +23 -0
  45. package/assets/icon-magnifying-glass.svg +5 -0
  46. package/assets/icon-warming-stripes.svg +13 -0
  47. package/assets/logo2.svg +31 -0
  48. package/components/AdvancedEditor/AdvancedEditor.tsx +4 -0
  49. package/components/AdvancedEditor/EmbedEditor.tsx +513 -0
  50. package/components/ComboBox/ComboBox.tsx +345 -0
  51. package/components/ComboBox/combobox.styles.css +185 -0
  52. package/components/ComboBox/index.ts +1 -0
  53. package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
  54. package/components/DataTable/DataTable.tsx +132 -58
  55. package/components/DataTable/data-table.css +216 -215
  56. package/components/DataTable/helpers/getSeriesName.ts +6 -0
  57. package/components/DataTable/helpers/mapCellMatrix.tsx +14 -6
  58. package/components/EditorPanel/ColumnsEditor.tsx +37 -19
  59. package/components/EditorPanel/DataTableEditor.tsx +51 -25
  60. package/components/EditorPanel/EditorPanel.styles.css +16 -0
  61. package/components/EditorPanel/EditorPanel.tsx +144 -0
  62. package/components/EditorPanel/EditorPanelDispatch.tsx +75 -0
  63. package/components/EditorPanel/FieldSetWrapper.tsx +66 -23
  64. package/components/EditorPanel/Inputs.tsx +33 -7
  65. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +14 -6
  66. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +240 -175
  67. package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
  68. package/components/EditorPanel/sections/VisualSection.tsx +169 -0
  69. package/components/Filters/Filters.tsx +31 -5
  70. package/components/Filters/helpers/getNestedOptions.ts +2 -1
  71. package/components/Filters/helpers/handleSorting.ts +1 -1
  72. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +84 -2
  73. package/components/Layout/components/Visualization/index.tsx +27 -1
  74. package/components/Layout/components/Visualization/visualizations.scss +7 -0
  75. package/components/Legend/Legend.Gradient.tsx +1 -1
  76. package/components/MediaControls.tsx +53 -28
  77. package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
  78. package/components/_stories/DataTable.stories.tsx +1 -0
  79. package/components/ui/Icon.tsx +3 -1
  80. package/components/ui/Title/index.tsx +30 -2
  81. package/components/ui/Title/title.styles.css +42 -0
  82. package/data/colorPalettes.ts +18 -5
  83. package/data/mapColorPalettes.ts +10 -0
  84. package/devTemplate/dev.js +235 -0
  85. package/devTemplate/index.html +30 -0
  86. package/devTemplate/preview.html +1503 -0
  87. package/devTemplate/sidebar.css +151 -0
  88. package/dist/cove-main.css +2803 -4448
  89. package/dist/cove-main.css.map +1 -1
  90. package/generateViteConfig.js +118 -2
  91. package/helpers/DataTransform.ts +1 -5
  92. package/helpers/addValuesToFilters.ts +6 -1
  93. package/helpers/cove/date.ts +33 -1
  94. package/helpers/cove/string.ts +29 -0
  95. package/helpers/coveUpdateWorker.ts +21 -12
  96. package/helpers/embed/embedCodeGenerator.ts +80 -0
  97. package/helpers/embed/embedHelper.js +158 -0
  98. package/helpers/embed/filterUtils.ts +121 -0
  99. package/helpers/embed/index.ts +21 -0
  100. package/helpers/embed/urlValidation.ts +119 -0
  101. package/helpers/filterVizData.ts +6 -1
  102. package/helpers/getFileExtension.ts +0 -6
  103. package/helpers/getUniqueValues.ts +19 -0
  104. package/helpers/hashObj.ts +25 -0
  105. package/helpers/isRightAlignedTableValue.js +5 -0
  106. package/helpers/metrics/helpers.ts +1 -0
  107. package/helpers/metrics/types.ts +3 -0
  108. package/helpers/palettes/colorDistributions.ts +1 -1
  109. package/helpers/palettes/utils.ts +12 -12
  110. package/helpers/parseCsvWithQuotes.ts +15 -14
  111. package/helpers/pivotData.ts +2 -2
  112. package/helpers/prepareScreenshot.ts +288 -0
  113. package/helpers/queryStringUtils.ts +29 -0
  114. package/helpers/testing.ts +44 -0
  115. package/helpers/tests/DataTransform.test.ts +125 -0
  116. package/helpers/tests/date.test.ts +64 -0
  117. package/helpers/tests/prepareScreenshot.test.ts +414 -0
  118. package/helpers/tests/queryStringUtils.test.ts +381 -0
  119. package/helpers/tests/testStandaloneBuild.ts +23 -5
  120. package/helpers/useDataVizClasses.ts +0 -1
  121. package/helpers/vegaConfig.ts +1 -1
  122. package/helpers/vegaConfigImport.ts +160 -0
  123. package/helpers/ver/4.26.1.ts +80 -0
  124. package/helpers/ver/4.26.2.ts +84 -0
  125. package/helpers/ver/tests/4.26.1.test.ts +105 -0
  126. package/helpers/ver/tests/4.26.2.test.ts +298 -0
  127. package/helpers/viewports.ts +2 -0
  128. package/hooks/useDataColumns.ts +63 -0
  129. package/hooks/useFilterManagement.ts +94 -0
  130. package/hooks/useLegendSeparators.ts +26 -0
  131. package/hooks/useListManagement.ts +192 -0
  132. package/package.json +29 -33
  133. package/styles/_button-section.scss +0 -3
  134. package/styles/v2/components/editor.scss +9 -9
  135. package/styles/v2/utils/_grid.scss +8 -3
  136. package/types/Annotation.ts +10 -11
  137. package/types/Axis.ts +1 -0
  138. package/types/ForecastingSeriesKey.ts +1 -0
  139. package/types/General.ts +2 -0
  140. package/types/MarkupInclude.ts +1 -0
  141. package/types/Palette.ts +21 -0
  142. package/types/Series.ts +3 -0
  143. package/types/Table.ts +1 -0
  144. package/types/Visualization.ts +7 -0
  145. package/types/VizFilter.ts +1 -0
  146. package/LICENSE +0 -201
  147. package/_stories/StoryRenderingTests.stories.tsx +0 -164
@@ -195,7 +195,7 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
195
195
  updateField={updateField}
196
196
  />
197
197
  )}
198
- {config?.visualizationType !== 'Sankey' && (
198
+ {config?.visualizationType !== 'Sankey' && config?.visualizationType !== 'Warming Stripes' && (
199
199
  <label onClick={e => e.preventDefault()}>
200
200
  <span className='edit-label column-heading mt-1'>Exclude Columns </span>
201
201
  <MultiSelect
@@ -283,11 +283,35 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
283
283
  <CheckBox
284
284
  value={config.table.showDownloadImgButton}
285
285
  fieldName='showDownloadImgButton'
286
- label='Display Image Button'
286
+ label='Display Image Download Link'
287
287
  section='table'
288
288
  updateField={updateField}
289
289
  />
290
290
  )}
291
+ {config.type !== 'table' && config.table.showDownloadImgButton && (
292
+ <CheckBox
293
+ value={config.table.includeContextInDownload}
294
+ fieldName='includeContextInDownload'
295
+ className='ms-4'
296
+ label='Include Heading & Context'
297
+ section='table'
298
+ updateField={updateField}
299
+ tooltip={
300
+ <Tooltip style={{ textTransform: 'none' }}>
301
+ <Tooltip.Target>
302
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
303
+ </Tooltip.Target>
304
+ <Tooltip.Content>
305
+ <p>
306
+ When enabled, the image download will include the section heading (H2 or H3) and any explanatory
307
+ paragraphs that appear immediately before the visualization. Be sure to test the image download on the
308
+ published page to ensure the correct context is included.
309
+ </p>
310
+ </Tooltip.Content>
311
+ </Tooltip>
312
+ }
313
+ />
314
+ )}
291
315
  <label>
292
316
  <span className='edit-label column-heading'>Table Cell Min Width</span>
293
317
  <input
@@ -296,7 +320,7 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
296
320
  onChange={e => updateField('table', null, 'cellMinWidth', e.target.value)}
297
321
  />
298
322
  </label>
299
- {config?.visualizationType !== 'Sankey' && (
323
+ {config?.visualizationType !== 'Sankey' && config?.visualizationType !== 'Warming Stripes' && (
300
324
  <Select
301
325
  value={config.table.groupBy}
302
326
  fieldName={'groupBy'}
@@ -322,28 +346,30 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
322
346
  }
323
347
  />
324
348
  )}
325
- <Select
326
- label='Pivot Column'
327
- tooltip={
328
- <Tooltip style={{ textTransform: 'none' }}>
329
- <Tooltip.Target>
330
- <Icon display='question' style={{ marginLeft: '0.5rem' }} />
331
- </Tooltip.Target>
332
- <Tooltip.Content>
333
- <p>Select a Column whos data values will be pivoted to Column Values.</p>
334
- </Tooltip.Content>
335
- </Tooltip>
336
- }
337
- value={config.table.pivot?.columnName}
338
- options={groupPivotColumns.filter(
339
- col => col !== config.table.groupBy && !(config.table.pivot?.valueColumns || []).includes(col)
340
- )}
341
- initial='-Select-'
342
- section='table'
343
- subsection='pivot'
344
- fieldName='columnName'
345
- updateField={updateField}
346
- />
349
+ {config.visualizationType !== 'Warming Stripes' && (
350
+ <Select
351
+ label='Pivot Column'
352
+ tooltip={
353
+ <Tooltip style={{ textTransform: 'none' }}>
354
+ <Tooltip.Target>
355
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
356
+ </Tooltip.Target>
357
+ <Tooltip.Content>
358
+ <p>Select a Column whos data values will be pivoted to Column Values.</p>
359
+ </Tooltip.Content>
360
+ </Tooltip>
361
+ }
362
+ value={config.table.pivot?.columnName}
363
+ options={groupPivotColumns.filter(
364
+ col => col !== config.table.groupBy && !(config.table.pivot?.valueColumns || []).includes(col)
365
+ )}
366
+ initial='-Select-'
367
+ section='table'
368
+ subsection='pivot'
369
+ fieldName='columnName'
370
+ updateField={updateField}
371
+ />
372
+ )}
347
373
  {config.table.pivot?.columnName && (
348
374
  <label>
349
375
  <span className='edit-label column-heading mt-1'>
@@ -421,3 +421,19 @@
421
421
  .editor-toggle svg path {
422
422
  fill: currentColor;
423
423
  }
424
+
425
+ /* Shared styles consolidated from package-specific EditorPanel.styles.css files */
426
+ /* Previously duplicated in: data-bite, filtered-text, markup-include */
427
+ .editor-panel .condition-section :is(label) {
428
+ font-size: 1em;
429
+ }
430
+
431
+ :is(span).edit-label {
432
+ margin-bottom: 0.3em;
433
+ display: block;
434
+ }
435
+
436
+ .react-tooltip {
437
+ position: absolute;
438
+ width: 250px;
439
+ }
@@ -0,0 +1,144 @@
1
+ import { useState, useEffect, useCallback, useRef, ReactNode } from 'react'
2
+ import { cloneConfig } from '../../helpers/cloneConfig'
3
+ import ErrorBoundary from '../ErrorBoundary'
4
+ import Layout from '../Layout'
5
+ import './EditorPanel.styles.css'
6
+
7
+ export interface BaseEditorPanelProps<TConfig = any> {
8
+ config: TConfig
9
+ updateConfig: (config: TConfig) => void
10
+ loading?: boolean
11
+ setParentConfig?: (config: TConfig) => void
12
+ isDashboard?: boolean
13
+ title: string
14
+ children: (props: EditorPanelChildProps<TConfig>) => ReactNode
15
+ initialDisplayPanel?: boolean
16
+ }
17
+
18
+ export interface EditorPanelChildProps<TConfig = any> {
19
+ config: TConfig
20
+ updateConfig: (config: TConfig) => void
21
+ displayPanel: boolean
22
+ convertStateToConfig: () => TConfig
23
+ }
24
+
25
+ /**
26
+ * Base EditorPanel component that provides shared functionality for all COVE visualization editors.
27
+ * Handles common patterns like panel display state, parent config syncing, and layout structure.
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * <EditorPanel
32
+ * config={config}
33
+ * updateConfig={updateConfig}
34
+ * loading={loading}
35
+ * setParentConfig={setParentConfig}
36
+ * isDashboard={isDashboard}
37
+ * title="Configure My Visualization"
38
+ * >
39
+ * {() => (
40
+ * <Accordion>
41
+ * <Accordion.Section title="General">
42
+ * // Your configuration UI here
43
+ * </Accordion.Section>
44
+ * </Accordion>
45
+ * )}
46
+ * </EditorPanel>
47
+ * ```
48
+ */
49
+ export function EditorPanel<TConfig = any>({
50
+ config,
51
+ updateConfig,
52
+ loading = false,
53
+ setParentConfig,
54
+ isDashboard,
55
+ title,
56
+ children,
57
+ initialDisplayPanel = true
58
+ }: BaseEditorPanelProps<TConfig>) {
59
+ const [displayPanel, setDisplayPanel] = useState(initialDisplayPanel)
60
+ const prevConfigRef = useRef<string>()
61
+
62
+ /**
63
+ * Converts current config to a clean state suitable for parent consumption.
64
+ * Removes runtime-only properties like 'newViz' and 'runtime'.
65
+ * In dashboard context, preserve 'editing' as it's managed by the dashboard.
66
+ */
67
+ const convertStateToConfig = useCallback((): TConfig => {
68
+ const strippedState = cloneConfig(config)
69
+ delete strippedState.newViz
70
+ delete strippedState.runtime
71
+ // Only delete editing flag if NOT in a dashboard context
72
+ if (!isDashboard) {
73
+ delete strippedState.editing
74
+ }
75
+ return strippedState
76
+ }, [config, isDashboard])
77
+
78
+ /**
79
+ * Sync config changes up to parent component when setParentConfig is provided.
80
+ * This is typically used in dashboard/editor contexts where the parent needs to track changes.
81
+ * Uses ref to prevent infinite loops by only syncing when config content actually changes.
82
+ */
83
+ useEffect(() => {
84
+ if (setParentConfig) {
85
+ const strippedState = cloneConfig(config)
86
+ delete strippedState.newViz
87
+ delete strippedState.runtime
88
+ // Only delete editing flag if NOT in a dashboard context
89
+ if (!isDashboard) {
90
+ delete strippedState.editing
91
+ }
92
+
93
+ // Only call setParentConfig if the config content actually changed
94
+ const configString = JSON.stringify(strippedState)
95
+ if (prevConfigRef.current !== configString) {
96
+ prevConfigRef.current = configString
97
+ setParentConfig(strippedState)
98
+ }
99
+ }
100
+ }, [config, setParentConfig, isDashboard])
101
+
102
+ /**
103
+ * Toggle the editor panel visibility and update config to reflect the change.
104
+ * In dashboard context, also sets editing to false to return to dashboard editor.
105
+ */
106
+ const onBackClick = () => {
107
+ const newDisplayPanel = !displayPanel
108
+ setDisplayPanel(newDisplayPanel)
109
+ const newConfig: TConfig = {
110
+ ...config,
111
+ showEditorPanel: newDisplayPanel
112
+ }
113
+ // If in dashboard mode, set editing to false to return to dashboard editor
114
+ if (isDashboard) {
115
+ (newConfig as any).editing = false
116
+ }
117
+
118
+ // Update local config - the useEffect will handle syncing to parent
119
+ updateConfig(newConfig)
120
+ }
121
+
122
+ // Don't render if loading and panel should be hidden
123
+ if (loading && !(config as any)?.showEditorPanel) return null
124
+
125
+ return (
126
+ <ErrorBoundary component='EditorPanel'>
127
+ <Layout.Sidebar
128
+ displayPanel={displayPanel}
129
+ isDashboard={isDashboard || false}
130
+ title={title}
131
+ onBackClick={onBackClick}
132
+ >
133
+ {children({
134
+ config,
135
+ updateConfig,
136
+ displayPanel,
137
+ convertStateToConfig
138
+ })}
139
+ </Layout.Sidebar>
140
+ </ErrorBoundary>
141
+ )
142
+ }
143
+
144
+ export default EditorPanel
@@ -0,0 +1,75 @@
1
+ import { ReactNode } from 'react'
2
+ import ErrorBoundary from '../ErrorBoundary'
3
+ import Layout from '../Layout'
4
+
5
+ export interface EditorPanelDispatchProps<TState = any, TAction = any> {
6
+ state: TState
7
+ dispatch: React.Dispatch<TAction>
8
+ title: string
9
+ children: (props: EditorPanelDispatchChildProps<TState, TAction>) => ReactNode
10
+ showEditorPanelKey?: keyof TState
11
+ toggleActionType?: string
12
+ isDashboard?: boolean
13
+ }
14
+
15
+ export interface EditorPanelDispatchChildProps<TState = any, TAction = any> {
16
+ state: TState
17
+ dispatch: React.Dispatch<TAction>
18
+ }
19
+
20
+ /**
21
+ * Base EditorPanel component for packages using dispatch/reducer pattern (e.g., data-table)
22
+ *
23
+ * Provides common wrapper functionality including:
24
+ * - ErrorBoundary for error handling
25
+ * - Layout.Sidebar for consistent panel display
26
+ * - State management for panel visibility
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * <EditorPanelDispatch
31
+ * state={state}
32
+ * dispatch={dispatch}
33
+ * title='Configure Data Table'
34
+ * showEditorPanelKey='showEditorPanel'
35
+ * toggleActionType='SET_SHOW_EDITOR_PANEL'
36
+ * >
37
+ * {({ state, dispatch }) => (
38
+ * <Accordion>
39
+ * // Your editor content here
40
+ * </Accordion>
41
+ * )}
42
+ * </EditorPanelDispatch>
43
+ * ```
44
+ */
45
+ export function EditorPanelDispatch<TState = any, TAction = any>({
46
+ state,
47
+ dispatch,
48
+ title,
49
+ children,
50
+ showEditorPanelKey = 'showEditorPanel' as keyof TState,
51
+ toggleActionType = 'SET_SHOW_EDITOR_PANEL',
52
+ isDashboard = false
53
+ }: EditorPanelDispatchProps<TState, TAction>) {
54
+ const showEditorPanel = state[showEditorPanelKey] as boolean
55
+
56
+ const onBackClick = () => {
57
+ dispatch({
58
+ type: toggleActionType,
59
+ payload: !showEditorPanel
60
+ } as TAction)
61
+ }
62
+
63
+ return (
64
+ <ErrorBoundary component='EditorPanel'>
65
+ <Layout.Sidebar
66
+ title={title}
67
+ onBackClick={onBackClick}
68
+ displayPanel={showEditorPanel}
69
+ isDashboard={isDashboard}
70
+ >
71
+ {children({ state, dispatch })}
72
+ </Layout.Sidebar>
73
+ </ErrorBoundary>
74
+ )
75
+ }
@@ -9,42 +9,85 @@ type FieldSetProps = {
9
9
  controls: OpenControls
10
10
  deleteField: Function
11
11
  children: React.ReactNode
12
+ draggable?: boolean
12
13
  }
13
14
 
14
- const FieldSet: React.FC<FieldSetProps> = ({ fieldName, fieldKey, fieldType, controls, deleteField, children }) => {
15
+ const FieldSet: React.FC<FieldSetProps> = ({
16
+ fieldName,
17
+ fieldKey,
18
+ fieldType,
19
+ controls,
20
+ deleteField,
21
+ children,
22
+ draggable = false
23
+ }) => {
15
24
  const [openControls, setOpenControls] = controls
16
25
  const show = openControls[fieldKey]
17
26
  const setShow = (key, value) => {
18
27
  setOpenControls({ ...openControls, [key]: value })
19
28
  }
20
29
 
21
- if (!show)
30
+ // Markup for non-draggable items
31
+ if (!draggable) {
32
+ if (!show)
33
+ return (
34
+ <div className='mb-1'>
35
+ <button type='button' className='btn btn-light' onClick={() => setShow(fieldKey, true)}>
36
+ <Icon display='caretDown' />
37
+ </button>
38
+ <span> {fieldName ? `${fieldName}` : 'New ' + fieldType}</span>
39
+ </div>
40
+ )
22
41
  return (
23
- <div className='mb-1'>
24
- <button className='btn btn-light' onClick={() => setShow(fieldKey, true)}>
25
- <Icon display='caretDown' />
26
- </button>
27
- <span> {fieldName ? `${fieldName}` : 'New ' + fieldType}</span>
28
- </div>
42
+ <fieldset className='edit-block mb-1' key={fieldKey}>
43
+ <div className='d-flex justify-content-between'>
44
+ <button type='button' className='btn btn-light' onClick={() => setShow(fieldKey, false)}>
45
+ <Icon display='caretUp' />
46
+ </button>
47
+ <button
48
+ type='button'
49
+ className='btn btn-danger btn-sm'
50
+ onClick={event => {
51
+ event.preventDefault()
52
+ deleteField()
53
+ }}
54
+ >
55
+ Remove
56
+ </button>
57
+ </div>
58
+ {children}
59
+ </fieldset>
29
60
  )
61
+ }
62
+
63
+ // Draggable fieldset
30
64
  return (
31
- <fieldset className='edit-block mb-1' key={fieldKey}>
32
- <div className='d-flex justify-content-between'>
33
- <button className='btn btn-light' onClick={() => setShow(fieldKey, false)}>
34
- <Icon display='caretUp' />
35
- </button>
36
- <button
37
- className='btn btn-danger btn-sm'
38
- onClick={event => {
39
- event.preventDefault()
40
- deleteField()
41
- }}
42
- >
43
- Remove
65
+ <div className='editor-field-item'>
66
+ <div className='editor-field-item__header'>
67
+ <Icon display='move' size={15} style={{ marginRight: '0.5rem' }} />
68
+ <button type='button' className='btn btn-light' onClick={() => setShow(fieldKey, !show)}>
69
+ <Icon display={show ? 'caretUp' : 'caretDown'} size={20} />
44
70
  </button>
71
+ <span className='editor-field-item__name'>{fieldName ? `${fieldName}` : 'New ' + fieldType}</span>
45
72
  </div>
46
- {children}
47
- </fieldset>
73
+ {show && (
74
+ <div className='editor-field-item__content'>
75
+ <div className='editor-field-item__remove-wrapper'>
76
+ <button
77
+ type='button'
78
+ className='btn btn-danger btn-sm'
79
+ onClick={event => {
80
+ event.preventDefault()
81
+ deleteField()
82
+ }}
83
+ >
84
+ Remove
85
+ </button>
86
+ </div>
87
+ {children}
88
+ </div>
89
+ )}
90
+ </div>
48
91
  )
49
92
  }
50
93
 
@@ -1,4 +1,4 @@
1
- import { memo, useEffect, useState } from 'react'
1
+ import { memo, useEffect, useState, useMemo } from 'react'
2
2
  import { useDebounce } from 'use-debounce'
3
3
  import { DROPDOWN_STYLES } from '../Filters/components/Dropdown'
4
4
 
@@ -50,6 +50,13 @@ const TextField = memo((props: TextFieldProps) => {
50
50
  const [value, setValue] = useState(stateValue)
51
51
  const [debouncedValue] = useDebounce(value, 500)
52
52
 
53
+ // Generate unique ID for accessibility
54
+ const inputId = useMemo(() => {
55
+ const sectionPart = section ?? 'root'
56
+ const subsectionPart = subsection ?? 'none'
57
+ return attributes.id || `input-${sectionPart}-${subsectionPart}-${fieldName}`
58
+ }, [section, subsection, fieldName, attributes.id])
59
+
53
60
  useEffect(() => {
54
61
  if ('string' === typeof debouncedValue && stateValue !== debouncedValue) {
55
62
  updateField(section, subsection, fieldName, debouncedValue, i)
@@ -74,25 +81,25 @@ const TextField = memo((props: TextFieldProps) => {
74
81
  }
75
82
  }
76
83
 
77
- let formElement = <input type='text' name={name} onChange={onChange} {...attributes} value={value} />
84
+ let formElement = <input type='text' id={inputId} name={name} onChange={onChange} {...attributes} value={value} />
78
85
 
79
86
  if ('textarea' === type) {
80
- formElement = <textarea name={name} onChange={onChange} {...attributes} value={value}></textarea>
87
+ formElement = <textarea id={inputId} name={name} onChange={onChange} {...attributes} value={value}></textarea>
81
88
  }
82
89
 
83
90
  if ('number' === type) {
84
- formElement = <input type='number' name={name} onChange={onChange} {...attributes} value={value} />
91
+ formElement = <input type='number' id={inputId} name={name} onChange={onChange} {...attributes} value={value} />
85
92
  }
86
93
 
87
94
  if ('date' === type) {
88
- formElement = <input type='date' name={name} onChange={onChange} {...attributes} value={value} />
95
+ formElement = <input type='date' id={inputId} name={name} onChange={onChange} {...attributes} value={value} />
89
96
  }
90
97
  if (!display) {
91
98
  return <></>
92
99
  }
93
100
 
94
101
  return (
95
- <label>
102
+ <label htmlFor={inputId}>
96
103
  <span className='edit-label column-heading'>
97
104
  {label}
98
105
  {tooltip}
@@ -114,11 +121,20 @@ const CheckBox = memo((props: CheckboxProps) => {
114
121
  updateField,
115
122
  ...attributes
116
123
  } = props
124
+
125
+ // Generate unique ID for accessibility
126
+ const inputId = useMemo(() => {
127
+ const sectionPart = section ?? 'root'
128
+ const subsectionPart = subsection ?? 'none'
129
+ return attributes.id || `checkbox-${sectionPart}-${subsectionPart}-${fieldName}`
130
+ }, [section, subsection, fieldName, attributes.id])
131
+
117
132
  if (!display) {
118
133
  return <></>
119
134
  }
120
135
  return (
121
136
  <label
137
+ htmlFor={inputId}
122
138
  className='checkbox column-heading'
123
139
  onClick={e => {
124
140
  if (!['SPAN', 'INPUT'].includes(e.target.nodeName)) {
@@ -128,6 +144,7 @@ const CheckBox = memo((props: CheckboxProps) => {
128
144
  >
129
145
  <input
130
146
  type='checkbox'
147
+ id={inputId}
131
148
  className='edit-checkbox'
132
149
  name={fieldName}
133
150
  checked={value}
@@ -172,6 +189,14 @@ const Select = memo((props: SelectProps) => {
172
189
  onChange: onChangeProp,
173
190
  ...attributes
174
191
  } = props
192
+
193
+ // Generate unique ID for accessibility
194
+ const inputId = useMemo(() => {
195
+ const sectionPart = section ?? 'root'
196
+ const subsectionPart = subsection ?? 'none'
197
+ return attributes.id || `select-${sectionPart}-${subsectionPart}-${fieldName}`
198
+ }, [section, subsection, fieldName, attributes.id])
199
+
175
200
  const optionsJsx = options?.map((option, index) => {
176
201
  if (typeof option === 'string') {
177
202
  return (
@@ -200,12 +225,13 @@ const Select = memo((props: SelectProps) => {
200
225
  }
201
226
 
202
227
  return (
203
- <label style={disabled ? { opacity: 0.6, pointerEvents: 'none' } : {}}>
228
+ <label htmlFor={inputId} style={disabled ? { opacity: 0.6, pointerEvents: 'none' } : {}}>
204
229
  <span className='edit-label'>
205
230
  {label}
206
231
  {tooltip}
207
232
  </span>
208
233
  <select
234
+ id={inputId}
209
235
  className={`cove-form-select ${required && !value ? 'warning' : ''} ${DROPDOWN_STYLES}`}
210
236
  name={fieldName}
211
237
  value={value}
@@ -16,6 +16,7 @@ type NestedDropdownEditorProps = {
16
16
  updateField: Function
17
17
  updateFilterStyle: Function
18
18
  handleGroupingCustomOrder: (index1: number, index2: number) => void
19
+ onNestedDragAreaHover?: (isHovering: boolean) => void
19
20
  }
20
21
 
21
22
  const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
@@ -25,7 +26,8 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
25
26
  handleNameChange: handleGroupColumnNameChange,
26
27
  filterIndex,
27
28
  rawData,
28
- updateField
29
+ updateField,
30
+ onNestedDragAreaHover
29
31
  }) => {
30
32
  const filter = config.filters[filterIndex]
31
33
  const subGrouping = filter?.subGrouping
@@ -159,7 +161,10 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
159
161
  <Select
160
162
  label='Filter Grouping'
161
163
  value={filter.columnName}
162
- options={[{ value: '', label: '- Select Option -' }, ...columnNameOptions.map(opt => ({ value: opt, label: opt }))]}
164
+ options={[
165
+ { value: '', label: '- Select Option -' },
166
+ ...columnNameOptions.map(opt => ({ value: opt, label: opt }))
167
+ ]}
163
168
  onChange={e => handleGroupColumnNameChange(e.target.value)}
164
169
  />
165
170
 
@@ -168,9 +173,7 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
168
173
  value={subGrouping?.columnName ?? ''}
169
174
  options={[
170
175
  { value: '', label: '- Select Option -' },
171
- ...columnNameOptions
172
- .filter(option => option !== filter.columnName)
173
- .map(opt => ({ value: opt, label: opt }))
176
+ ...columnNameOptions.filter(option => option !== filter.columnName).map(opt => ({ value: opt, label: opt }))
174
177
  ]}
175
178
  onChange={e => {
176
179
  handleSubGroupColumnNameChange(e.target.value)
@@ -222,7 +225,11 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
222
225
  onChange={e => handleGroupingOrderBy(e.target.value as OrderBy)}
223
226
  />
224
227
  {filter.order === 'cust' && (
225
- <FilterOrder orderedValues={filter.orderedValues} handleFilterOrder={handleGroupingCustomOrder} />
228
+ <FilterOrder
229
+ orderedValues={filter.orderedValues}
230
+ handleFilterOrder={handleGroupingCustomOrder}
231
+ onNestedDragAreaHover={onNestedDragAreaHover}
232
+ />
226
233
  )}
227
234
  </div>
228
235
 
@@ -247,6 +254,7 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
247
254
  handleFilterOrder={(sourceIndex, destinationIndex) => {
248
255
  handleSubGroupingCustomOrder(sourceIndex, destinationIndex, orderedSubGroupValues, groupName)
249
256
  }}
257
+ onNestedDragAreaHover={onNestedDragAreaHover}
250
258
  />
251
259
  </div>
252
260
  )