@cdc/core 4.25.10 → 4.26.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 (134) hide show
  1. package/_stories/Gallery.Charts.stories.tsx +307 -0
  2. package/_stories/Gallery.DataBite.stories.tsx +72 -0
  3. package/_stories/Gallery.Maps.stories.tsx +230 -0
  4. package/_stories/Gallery.WaffleChart.stories.tsx +187 -0
  5. package/_stories/PageART.stories.tsx +192 -0
  6. package/_stories/PageBRFSS.stories.tsx +289 -0
  7. package/_stories/PageCancerRegistries.stories.tsx +199 -0
  8. package/_stories/PageEasternEquineEncephalitis.stories.tsx +202 -0
  9. package/_stories/PageExcessiveAlcoholUse.stories.tsx +196 -0
  10. package/_stories/PageMaternalMortality.stories.tsx +192 -0
  11. package/_stories/PageOralHealth.stories.tsx +196 -0
  12. package/_stories/PageRespiratory.stories.tsx +332 -0
  13. package/_stories/PageSmokingTobacco.stories.tsx +195 -0
  14. package/_stories/PageStateDiabetesProfiles.stories.tsx +196 -0
  15. package/_stories/PageWastewater.stories.tsx +463 -0
  16. package/_stories/StoryRenderingTests.stories.tsx +164 -0
  17. package/assets/icon-magnifying-glass.svg +5 -0
  18. package/assets/icon-warming-stripes.svg +13 -0
  19. package/components/AdvancedEditor/AdvancedEditor.tsx +7 -1
  20. package/components/AdvancedEditor/EmbedEditor.tsx +281 -0
  21. package/components/ComboBox/ComboBox.tsx +345 -0
  22. package/components/ComboBox/combobox.styles.css +185 -0
  23. package/components/ComboBox/index.ts +1 -0
  24. package/components/CustomColorsEditor/CustomColorsEditor.css +299 -0
  25. package/components/CustomColorsEditor/CustomColorsEditor.tsx +209 -0
  26. package/components/CustomColorsEditor/index.ts +1 -0
  27. package/components/DataTable/DataTable.tsx +132 -58
  28. package/components/DataTable/DataTableStandAlone.tsx +8 -3
  29. package/components/DataTable/components/DataTableEditorPanel.tsx +12 -2
  30. package/components/DataTable/data-table.css +217 -210
  31. package/components/DataTable/helpers/mapCellMatrix.tsx +28 -9
  32. package/components/DataTable/helpers/standardizeState.js +2 -2
  33. package/components/DataTable/helpers/tests/standardizeState.test.js +54 -0
  34. package/components/EditorPanel/ColumnsEditor.tsx +37 -19
  35. package/components/EditorPanel/DataTableEditor.tsx +54 -28
  36. package/components/EditorPanel/EditorPanel.styles.css +439 -0
  37. package/components/EditorPanel/EditorPanel.tsx +144 -0
  38. package/components/EditorPanel/EditorPanelDispatch.tsx +75 -0
  39. package/components/EditorPanel/FieldSetWrapper.tsx +66 -23
  40. package/components/EditorPanel/FootnotesEditor.tsx +44 -37
  41. package/components/EditorPanel/Inputs.tsx +44 -8
  42. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +35 -62
  43. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +246 -175
  44. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +61 -22
  45. package/components/EditorPanel/sections/VisualSection.tsx +169 -0
  46. package/components/Filters/Filters.tsx +57 -10
  47. package/components/Filters/components/Dropdown.tsx +6 -1
  48. package/components/Filters/helpers/getNestedOptions.ts +2 -1
  49. package/components/Filters/helpers/handleSorting.ts +1 -1
  50. package/components/Footnotes/Footnotes.tsx +35 -25
  51. package/components/Footnotes/FootnotesStandAlone.tsx +42 -6
  52. package/components/HeaderThemeSelector/HeaderThemeSelector.css +43 -0
  53. package/components/HeaderThemeSelector/HeaderThemeSelector.stories.tsx +74 -0
  54. package/components/HeaderThemeSelector/HeaderThemeSelector.tsx +61 -0
  55. package/components/HeaderThemeSelector/index.ts +2 -0
  56. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +82 -0
  57. package/components/Layout/components/Visualization/index.tsx +16 -1
  58. package/components/Layout/components/Visualization/visualizations.scss +7 -0
  59. package/components/Layout/styles/editor.scss +2 -1
  60. package/components/Legend/Legend.Gradient.tsx +1 -1
  61. package/components/Loader/Loader.tsx +1 -1
  62. package/components/MediaControls.tsx +63 -34
  63. package/components/PaletteConversionModal.tsx +7 -4
  64. package/components/PaletteSelector/PaletteSelector.css +49 -6
  65. package/components/Table/components/Cell.tsx +23 -2
  66. package/components/Table/components/Row.tsx +5 -3
  67. package/components/_stories/Filters.stories.tsx +20 -1
  68. package/components/_stories/Footnotes.CSV.stories.tsx +247 -0
  69. package/components/_stories/Footnotes.stories.tsx +768 -3
  70. package/components/_stories/Inputs.stories.tsx +2 -2
  71. package/components/_stories/styles.scss +0 -1
  72. package/components/ui/Accordion.jsx +1 -1
  73. package/components/ui/Icon.tsx +3 -1
  74. package/components/ui/Title/index.tsx +30 -2
  75. package/components/ui/Title/title.styles.css +42 -0
  76. package/components/ui/accordion.styles.css +57 -0
  77. package/data/chartColorPalettes.ts +1 -1
  78. package/dist/cove-main.css +75 -6
  79. package/dist/cove-main.css.map +1 -1
  80. package/generateViteConfig.js +8 -1
  81. package/helpers/addValuesToFilters.ts +11 -1
  82. package/helpers/constants.ts +37 -0
  83. package/helpers/cove/number.ts +33 -12
  84. package/helpers/coveUpdateWorker.ts +20 -11
  85. package/helpers/embedCodeGenerator.ts +109 -0
  86. package/helpers/fetchRemoteData.ts +3 -15
  87. package/helpers/getUniqueValues.ts +19 -0
  88. package/helpers/hashObj.ts +25 -0
  89. package/helpers/isRightAlignedTableValue.js +5 -0
  90. package/helpers/markupProcessor.ts +27 -12
  91. package/helpers/mergeCustomOrderValues.ts +37 -0
  92. package/helpers/metrics/helpers.ts +1 -0
  93. package/helpers/parseCsvWithQuotes.ts +65 -0
  94. package/helpers/pivotData.ts +2 -2
  95. package/helpers/prepareScreenshot.ts +268 -0
  96. package/helpers/queryStringUtils.ts +29 -0
  97. package/helpers/testing.ts +17 -4
  98. package/helpers/tests/prepareScreenshot.test.ts +414 -0
  99. package/helpers/tests/queryStringUtils.test.ts +381 -0
  100. package/helpers/tests/testStandaloneBuild.ts +23 -5
  101. package/helpers/useDataVizClasses.ts +0 -1
  102. package/helpers/ver/4.25.11.ts +13 -0
  103. package/helpers/ver/4.26.1.ts +80 -0
  104. package/helpers/viewports.ts +2 -0
  105. package/hooks/useDataColumns.ts +63 -0
  106. package/hooks/useFilterManagement.ts +94 -0
  107. package/hooks/useLegendSeparators.ts +26 -0
  108. package/hooks/useListManagement.ts +192 -0
  109. package/package.json +6 -4
  110. package/styles/_button-section.scss +0 -3
  111. package/styles/_common-components.css +73 -0
  112. package/styles/_global.scss +25 -5
  113. package/styles/base.scss +0 -50
  114. package/styles/cove-main.scss +3 -1
  115. package/styles/filters.scss +10 -3
  116. package/styles/v2/base/index.scss +0 -1
  117. package/styles/v2/components/editor.scss +14 -6
  118. package/styles/v2/utils/_breakpoints.scss +1 -1
  119. package/styles/v2/utils/index.scss +0 -1
  120. package/styles/waiting.scss +1 -1
  121. package/types/Axis.ts +1 -0
  122. package/types/ForecastingSeriesKey.ts +1 -0
  123. package/types/MarkupInclude.ts +5 -3
  124. package/types/MarkupVariable.ts +1 -1
  125. package/types/Series.ts +3 -0
  126. package/types/Table.ts +1 -0
  127. package/types/Visualization.ts +1 -0
  128. package/types/VizFilter.ts +2 -0
  129. package/LICENSE +0 -201
  130. package/styles/_mixins.scss +0 -13
  131. package/styles/_typography.scss +0 -0
  132. package/styles/v2/base/_typography.scss +0 -0
  133. package/styles/v2/components/guidance-block.scss +0 -74
  134. package/styles/v2/utils/_functions.scss +0 -0
@@ -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
 
@@ -3,8 +3,7 @@ import { UpdateFieldFunc } from '../../types/UpdateFieldFunc'
3
3
  import _ from 'lodash'
4
4
  import Footnotes, { Footnote } from '../../types/Footnotes'
5
5
  import { footnotesSymbols } from '../../helpers/footnoteSymbols'
6
- import InputSelect from '../inputs/InputSelect'
7
- import { TextField } from './Inputs'
6
+ import { TextField, Select } from './Inputs'
8
7
  import { Datasets } from '@cdc/core/types/DataSet'
9
8
  import DataTransform from '../../helpers/DataTransform'
10
9
  import fetchRemoteData from '../../helpers/fetchRemoteData'
@@ -61,14 +60,14 @@ const FootnotesEditor: React.FC<FootnotesEditorProps> = ({ config, updateField,
61
60
  updateField('footnotes', null, 'staticFootnotes', footnoteCopy)
62
61
  }
63
62
 
64
- const getOptions = (opts: string[]) => {
65
- return [['', '--Select--']].concat(opts.map(key => [key, key]))
63
+ const getSelectOptions = (opts: string[]) => {
64
+ return [{ value: '', label: '--Select--' }].concat(opts.map(key => ({ value: key, label: key })))
66
65
  }
67
66
 
68
67
  const dataColumns = footnotesConfig.dataKey
69
- ? getOptions(Object.keys(datasetsCache[footnotesConfig.dataKey]?.data?.[0] || {}))
68
+ ? getSelectOptions(Object.keys(datasetsCache[footnotesConfig.dataKey]?.data?.[0] || {}))
70
69
  : []
71
- const dataSetOptions = getOptions(Object.keys(datasetsCache))
70
+ const dataSetOptions = getSelectOptions(Object.keys(datasetsCache))
72
71
 
73
72
  const changeFootnoteDataKey = async value => {
74
73
  if (value) {
@@ -87,7 +86,7 @@ const FootnotesEditor: React.FC<FootnotesEditorProps> = ({ config, updateField,
87
86
  {loadingAPIData && <Loader fullScreen />}
88
87
  <em>Dynamic Footnotes</em>
89
88
  <div className='row border p-2'>
90
- <InputSelect
89
+ <Select
91
90
  label='Select a Footnote Dataset'
92
91
  value={footnotesConfig.dataKey}
93
92
  options={dataSetOptions}
@@ -100,7 +99,7 @@ const FootnotesEditor: React.FC<FootnotesEditorProps> = ({ config, updateField,
100
99
 
101
100
  {footnotesConfig.dataKey && (
102
101
  <div className='p-3'>
103
- <InputSelect
102
+ <Select
104
103
  label='Footnote Symbol Column'
105
104
  value={footnotesConfig.dynamicFootnotes?.symbolColumn}
106
105
  options={dataColumns}
@@ -109,7 +108,7 @@ const FootnotesEditor: React.FC<FootnotesEditorProps> = ({ config, updateField,
109
108
  fieldName='symbolColumn'
110
109
  updateField={updateField}
111
110
  />
112
- <InputSelect
111
+ <Select
113
112
  label='Footnote Text Column'
114
113
  value={footnotesConfig.dynamicFootnotes?.textColumn}
115
114
  options={dataColumns}
@@ -118,7 +117,7 @@ const FootnotesEditor: React.FC<FootnotesEditorProps> = ({ config, updateField,
118
117
  fieldName='textColumn'
119
118
  updateField={updateField}
120
119
  />
121
- <InputSelect
120
+ <Select
122
121
  label='Footnote Order Column'
123
122
  value={footnotesConfig.dynamicFootnotes?.orderColumn}
124
123
  options={dataColumns}
@@ -135,34 +134,42 @@ const FootnotesEditor: React.FC<FootnotesEditorProps> = ({ config, updateField,
135
134
 
136
135
  <em>Static Footnotes</em>
137
136
 
138
- {footnotesConfig.staticFootnotes?.map((note, index) => (
139
- <div key={index} className='row border p-2'>
140
- <div className='col-8'>
141
- <InputSelect
142
- label='Symbol'
143
- value={note.symbol}
144
- options={[['', '--Select--'], ...footnotesSymbols]}
145
- fieldName='symbol'
146
- updateField={(section, subsection, fieldName, value) =>
147
- updateStaticFootnote(index, { ...note, symbol: value })
148
- }
149
- />{' '}
150
- <TextField
151
- label='Text'
152
- value={note.text}
153
- fieldName='text'
154
- updateField={(section, subsection, fieldName, value) =>
155
- updateStaticFootnote(index, { ...note, text: value })
156
- }
157
- />
158
- </div>
159
- <div className='col-2 ms-4'>
160
- <button className='btn btn-danger p-1' onClick={() => deleteStaticFootnote(index)}>
161
- Delete
162
- </button>
137
+ {footnotesConfig.staticFootnotes?.map((note, index) => {
138
+ // Convert tuple format to {value, label} format for Select component
139
+ const symbolOptions = [
140
+ { value: '', label: '--Select--' },
141
+ ...footnotesSymbols.map(([value, label]) => ({ value, label }))
142
+ ]
143
+
144
+ return (
145
+ <div key={index} className='row border p-2'>
146
+ <div className='col-8'>
147
+ <Select
148
+ label='Symbol'
149
+ value={note.symbol}
150
+ options={symbolOptions}
151
+ fieldName='symbol'
152
+ updateField={(section, subsection, fieldName, value) =>
153
+ updateStaticFootnote(index, { ...note, symbol: value })
154
+ }
155
+ />{' '}
156
+ <TextField
157
+ label='Text'
158
+ value={note.text}
159
+ fieldName='text'
160
+ updateField={(section, subsection, fieldName, value) =>
161
+ updateStaticFootnote(index, { ...note, text: value })
162
+ }
163
+ />
164
+ </div>
165
+ <div className='col-2 ms-4'>
166
+ <button className='btn btn-danger p-1' onClick={() => deleteStaticFootnote(index)}>
167
+ Delete
168
+ </button>
169
+ </div>
163
170
  </div>
164
- </div>
165
- ))}
171
+ )
172
+ })}
166
173
  <button className='btn btn-primary' onClick={addStaticFootnote}>
167
174
  Add Static Footnote
168
175
  </button>
@@ -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}
@@ -149,6 +166,7 @@ export type SelectProps = {
149
166
  options?: string[] | { label: string; value: string }[]
150
167
  required?: boolean
151
168
  initial?: string
169
+ disabled?: boolean
152
170
 
153
171
  // all other props
154
172
  [x: string]: any
@@ -167,8 +185,18 @@ const Select = memo((props: SelectProps) => {
167
185
  tooltip,
168
186
  updateField,
169
187
  initial: initialValue,
188
+ disabled = false,
189
+ onChange: onChangeProp,
170
190
  ...attributes
171
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
+
172
200
  const optionsJsx = options?.map((option, index) => {
173
201
  if (typeof option === 'string') {
174
202
  return (
@@ -197,18 +225,26 @@ const Select = memo((props: SelectProps) => {
197
225
  }
198
226
 
199
227
  return (
200
- <label>
228
+ <label htmlFor={inputId} style={disabled ? { opacity: 0.6, pointerEvents: 'none' } : {}}>
201
229
  <span className='edit-label'>
202
230
  {label}
203
231
  {tooltip}
204
232
  </span>
205
233
  <select
234
+ id={inputId}
206
235
  className={`cove-form-select ${required && !value ? 'warning' : ''} ${DROPDOWN_STYLES}`}
207
236
  name={fieldName}
208
237
  value={value}
238
+ disabled={disabled}
209
239
  onChange={event => {
210
- updateField(section, subsection, fieldName, event.target.value)
240
+ if (updateField) {
241
+ updateField(section, subsection, fieldName, event.target.value)
242
+ }
243
+ if (onChangeProp) {
244
+ onChangeProp(event)
245
+ }
211
246
  }}
247
+ style={disabled ? { cursor: 'not-allowed', backgroundColor: '#e9ecef' } : {}}
212
248
  {...attributes}
213
249
  >
214
250
  {optionsJsx}