@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.
- package/_stories/Gallery.Charts.stories.tsx +307 -0
- package/_stories/Gallery.DataBite.stories.tsx +72 -0
- package/_stories/Gallery.Maps.stories.tsx +230 -0
- package/_stories/Gallery.WaffleChart.stories.tsx +187 -0
- package/_stories/PageART.stories.tsx +192 -0
- package/_stories/PageBRFSS.stories.tsx +289 -0
- package/_stories/PageCancerRegistries.stories.tsx +199 -0
- package/_stories/PageEasternEquineEncephalitis.stories.tsx +202 -0
- package/_stories/PageExcessiveAlcoholUse.stories.tsx +196 -0
- package/_stories/PageMaternalMortality.stories.tsx +192 -0
- package/_stories/PageOralHealth.stories.tsx +196 -0
- package/_stories/PageRespiratory.stories.tsx +332 -0
- package/_stories/PageSmokingTobacco.stories.tsx +195 -0
- package/_stories/PageStateDiabetesProfiles.stories.tsx +196 -0
- package/_stories/PageWastewater.stories.tsx +463 -0
- package/_stories/StoryRenderingTests.stories.tsx +164 -0
- package/assets/icon-magnifying-glass.svg +5 -0
- package/assets/icon-warming-stripes.svg +13 -0
- package/components/AdvancedEditor/AdvancedEditor.tsx +7 -1
- package/components/AdvancedEditor/EmbedEditor.tsx +281 -0
- package/components/ComboBox/ComboBox.tsx +345 -0
- package/components/ComboBox/combobox.styles.css +185 -0
- package/components/ComboBox/index.ts +1 -0
- package/components/CustomColorsEditor/CustomColorsEditor.css +299 -0
- package/components/CustomColorsEditor/CustomColorsEditor.tsx +209 -0
- package/components/CustomColorsEditor/index.ts +1 -0
- package/components/DataTable/DataTable.tsx +132 -58
- package/components/DataTable/DataTableStandAlone.tsx +8 -3
- package/components/DataTable/components/DataTableEditorPanel.tsx +12 -2
- package/components/DataTable/data-table.css +217 -210
- package/components/DataTable/helpers/mapCellMatrix.tsx +28 -9
- package/components/DataTable/helpers/standardizeState.js +2 -2
- package/components/DataTable/helpers/tests/standardizeState.test.js +54 -0
- package/components/EditorPanel/ColumnsEditor.tsx +37 -19
- package/components/EditorPanel/DataTableEditor.tsx +54 -28
- package/components/EditorPanel/EditorPanel.styles.css +439 -0
- package/components/EditorPanel/EditorPanel.tsx +144 -0
- package/components/EditorPanel/EditorPanelDispatch.tsx +75 -0
- package/components/EditorPanel/FieldSetWrapper.tsx +66 -23
- package/components/EditorPanel/FootnotesEditor.tsx +44 -37
- package/components/EditorPanel/Inputs.tsx +44 -8
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +35 -62
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +246 -175
- package/components/EditorPanel/components/MarkupVariablesEditor.tsx +61 -22
- package/components/EditorPanel/sections/VisualSection.tsx +169 -0
- package/components/Filters/Filters.tsx +57 -10
- package/components/Filters/components/Dropdown.tsx +6 -1
- package/components/Filters/helpers/getNestedOptions.ts +2 -1
- package/components/Filters/helpers/handleSorting.ts +1 -1
- package/components/Footnotes/Footnotes.tsx +35 -25
- package/components/Footnotes/FootnotesStandAlone.tsx +42 -6
- package/components/HeaderThemeSelector/HeaderThemeSelector.css +43 -0
- package/components/HeaderThemeSelector/HeaderThemeSelector.stories.tsx +74 -0
- package/components/HeaderThemeSelector/HeaderThemeSelector.tsx +61 -0
- package/components/HeaderThemeSelector/index.ts +2 -0
- package/components/Layout/components/Sidebar/components/sidebar.styles.scss +82 -0
- package/components/Layout/components/Visualization/index.tsx +16 -1
- package/components/Layout/components/Visualization/visualizations.scss +7 -0
- package/components/Layout/styles/editor.scss +2 -1
- package/components/Legend/Legend.Gradient.tsx +1 -1
- package/components/Loader/Loader.tsx +1 -1
- package/components/MediaControls.tsx +63 -34
- package/components/PaletteConversionModal.tsx +7 -4
- package/components/PaletteSelector/PaletteSelector.css +49 -6
- package/components/Table/components/Cell.tsx +23 -2
- package/components/Table/components/Row.tsx +5 -3
- package/components/_stories/Filters.stories.tsx +20 -1
- package/components/_stories/Footnotes.CSV.stories.tsx +247 -0
- package/components/_stories/Footnotes.stories.tsx +768 -3
- package/components/_stories/Inputs.stories.tsx +2 -2
- package/components/_stories/styles.scss +0 -1
- package/components/ui/Accordion.jsx +1 -1
- package/components/ui/Icon.tsx +3 -1
- package/components/ui/Title/index.tsx +30 -2
- package/components/ui/Title/title.styles.css +42 -0
- package/components/ui/accordion.styles.css +57 -0
- package/data/chartColorPalettes.ts +1 -1
- package/dist/cove-main.css +75 -6
- package/dist/cove-main.css.map +1 -1
- package/generateViteConfig.js +8 -1
- package/helpers/addValuesToFilters.ts +11 -1
- package/helpers/constants.ts +37 -0
- package/helpers/cove/number.ts +33 -12
- package/helpers/coveUpdateWorker.ts +20 -11
- package/helpers/embedCodeGenerator.ts +109 -0
- package/helpers/fetchRemoteData.ts +3 -15
- package/helpers/getUniqueValues.ts +19 -0
- package/helpers/hashObj.ts +25 -0
- package/helpers/isRightAlignedTableValue.js +5 -0
- package/helpers/markupProcessor.ts +27 -12
- package/helpers/mergeCustomOrderValues.ts +37 -0
- package/helpers/metrics/helpers.ts +1 -0
- package/helpers/parseCsvWithQuotes.ts +65 -0
- package/helpers/pivotData.ts +2 -2
- package/helpers/prepareScreenshot.ts +268 -0
- package/helpers/queryStringUtils.ts +29 -0
- package/helpers/testing.ts +17 -4
- package/helpers/tests/prepareScreenshot.test.ts +414 -0
- package/helpers/tests/queryStringUtils.test.ts +381 -0
- package/helpers/tests/testStandaloneBuild.ts +23 -5
- package/helpers/useDataVizClasses.ts +0 -1
- package/helpers/ver/4.25.11.ts +13 -0
- package/helpers/ver/4.26.1.ts +80 -0
- package/helpers/viewports.ts +2 -0
- package/hooks/useDataColumns.ts +63 -0
- package/hooks/useFilterManagement.ts +94 -0
- package/hooks/useLegendSeparators.ts +26 -0
- package/hooks/useListManagement.ts +192 -0
- package/package.json +6 -4
- package/styles/_button-section.scss +0 -3
- package/styles/_common-components.css +73 -0
- package/styles/_global.scss +25 -5
- package/styles/base.scss +0 -50
- package/styles/cove-main.scss +3 -1
- package/styles/filters.scss +10 -3
- package/styles/v2/base/index.scss +0 -1
- package/styles/v2/components/editor.scss +14 -6
- package/styles/v2/utils/_breakpoints.scss +1 -1
- package/styles/v2/utils/index.scss +0 -1
- package/styles/waiting.scss +1 -1
- package/types/Axis.ts +1 -0
- package/types/ForecastingSeriesKey.ts +1 -0
- package/types/MarkupInclude.ts +5 -3
- package/types/MarkupVariable.ts +1 -1
- package/types/Series.ts +3 -0
- package/types/Table.ts +1 -0
- package/types/Visualization.ts +1 -0
- package/types/VizFilter.ts +2 -0
- package/LICENSE +0 -201
- package/styles/_mixins.scss +0 -13
- package/styles/_typography.scss +0 -0
- package/styles/v2/base/_typography.scss +0 -0
- package/styles/v2/components/guidance-block.scss +0 -74
- 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> = ({
|
|
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
|
-
|
|
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
|
-
<
|
|
24
|
-
<
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
<
|
|
32
|
-
<div className='
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
{
|
|
47
|
-
|
|
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
|
|
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
|
|
65
|
-
return [
|
|
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
|
-
?
|
|
68
|
+
? getSelectOptions(Object.keys(datasetsCache[footnotesConfig.dataKey]?.data?.[0] || {}))
|
|
70
69
|
: []
|
|
71
|
-
const dataSetOptions =
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
|
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}
|