@cdc/data-bite 4.26.2 → 4.26.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.
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@cdc/data-bite",
3
- "version": "4.26.2",
3
+ "version": "4.26.3",
4
4
  "description": "React component for displaying a single piece of data in a card module",
5
5
  "license": "Apache-2.0",
6
6
  "author": "cooms13 <dmo7@cdc.gov>",
7
7
  "bugs": "https://github.com/CDCgov/cdc-open-viz/issues",
8
8
  "dependencies": {
9
- "@cdc/core": "^4.26.2",
9
+ "@cdc/core": "^4.26.3",
10
10
  "chroma-js": "^3.1.2",
11
11
  "html-react-parser": "^5.2.3",
12
12
  "lodash": "^4.17.23",
@@ -20,7 +20,7 @@
20
20
  "vite-plugin-css-injected-by-js": "^2.4.0",
21
21
  "vite-plugin-svgr": "^4.2.0"
22
22
  },
23
- "gitHead": "be3413e8e1149abf94225108f86a7910f56e0616",
23
+ "gitHead": "d50e45a074fbefa56cac904917e707d57f237737",
24
24
  "homepage": "https://github.com/CDCgov/cdc-open-viz#readme",
25
25
  "main": "dist/cdcdatabite",
26
26
  "moduleName": "CdcDataBite",
@@ -11,7 +11,7 @@ import Loading from '@cdc/core/components/Loading'
11
11
  import Title from '@cdc/core/components/ui/Title'
12
12
  import CircleCallout from './components/CircleCallout'
13
13
  import GradientBite from './components/GradientBite'
14
- import Layout from '@cdc/core/components/Layout'
14
+ import { VisualizationContainer, VisualizationContent } from '@cdc/core/components/Layout'
15
15
 
16
16
  // external
17
17
  import ResizeObserver from 'resize-observer-polyfill'
@@ -29,12 +29,13 @@ import { publish } from '@cdc/core/helpers/events'
29
29
  import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses'
30
30
  import cacheBustingString from '@cdc/core/helpers/cacheBustingString'
31
31
  import coveUpdateWorker from '@cdc/core/helpers/coveUpdateWorker'
32
+ import { backfillDefaults } from '@cdc/core/helpers/backfillDefaults'
32
33
  import { Config } from './types/Config'
33
34
  import dataBiteReducer from './store/db.reducer'
34
35
  import { IMAGE_POSITION_LEFT, IMAGE_POSITION_RIGHT, IMAGE_POSITION_TOP, IMAGE_POSITION_BOTTOM } from './constants'
35
36
 
36
37
  // images
37
- import CalloutFlag from './images/callout-flag.svg?url'
38
+ import CalloutFlag from '@cdc/core/assets/callout-flag.svg?url'
38
39
 
39
40
  import {
40
41
  DATA_FUNCTION_COUNT,
@@ -43,6 +44,7 @@ import {
43
44
  DATA_FUNCTION_MEDIAN,
44
45
  DATA_FUNCTION_MODE,
45
46
  DATA_FUNCTION_MIN,
47
+ DATA_FUNCTION_PASSTHROUGH,
46
48
  DATA_FUNCTION_RANGE,
47
49
  DATA_FUNCTION_SUM
48
50
  } from '@cdc/core/helpers/constants'
@@ -71,8 +73,37 @@ const CdcDataBite = (props: CdcDataBiteProps) => {
71
73
  interactionLabel = ''
72
74
  } = props
73
75
 
76
+ // Ensure imageData and dataFormat sub-fields are always defined before the reducer initializes.
77
+ // Defaults must match initial-state.js — updateConfig() will enforce them again once loading completes.
78
+ const safeConfigObj = {
79
+ ...defaults,
80
+ ...configObj,
81
+ imageData: {
82
+ ...defaults.imageData,
83
+ ...(configObj?.imageData || {}),
84
+ display: configObj?.imageData?.display ?? 'none',
85
+ prefix: configObj?.imageData?.prefix ?? ''
86
+ },
87
+ dataFormat: {
88
+ ...defaults.dataFormat,
89
+ ...(configObj?.dataFormat || {}),
90
+ prefix: configObj?.dataFormat?.prefix ?? '',
91
+ suffix: configObj?.dataFormat?.suffix ?? '%',
92
+ roundToPlace: configObj?.dataFormat?.roundToPlace ?? 0,
93
+ commas: configObj?.dataFormat?.commas ?? true
94
+ },
95
+ visual: {
96
+ ...defaults.visual,
97
+ ...(configObj?.visual || {})
98
+ },
99
+ general: {
100
+ ...defaults.general,
101
+ ...(configObj?.general || {})
102
+ }
103
+ }
104
+
74
105
  const initialState = {
75
- config: configObj ?? defaults,
106
+ config: safeConfigObj ?? defaults,
76
107
  loading: true,
77
108
  currentViewport: 'lg',
78
109
  coveLoadedHasRan: false,
@@ -111,12 +142,7 @@ const CdcDataBite = (props: CdcDataBiteProps) => {
111
142
  })
112
143
 
113
144
  const updateConfig = newConfig => {
114
- // Deeper copy
115
- Object.keys(defaults).forEach(key => {
116
- if (newConfig[key] && 'object' === typeof newConfig[key] && !Array.isArray(newConfig[key])) {
117
- newConfig[key] = { ...defaults[key], ...newConfig[key] }
118
- }
119
- })
145
+ backfillDefaults(newConfig, defaults)
120
146
 
121
147
  //Enforce default values that need to be calculated at runtime
122
148
  newConfig.runtime = {}
@@ -154,7 +180,8 @@ const CdcDataBite = (props: CdcDataBiteProps) => {
154
180
 
155
181
  if (response.dataUrl) {
156
182
  response.dataUrl = `${response.dataUrl}?${cacheBustingString()}`
157
- let newData = await fetchRemoteData(response.dataUrl)
183
+ let { data: newData, dataMetadata } = await fetchRemoteData(response.dataUrl)
184
+ response.dataMetadata = dataMetadata
158
185
 
159
186
  if (newData && response.dataDescription) {
160
187
  newData = transform.autoStandardize(newData)
@@ -170,6 +197,12 @@ const CdcDataBite = (props: CdcDataBiteProps) => {
170
197
 
171
198
  const processedConfig = { ...coveUpdateWorker(response) }
172
199
 
200
+ // Migrate: borders always showed in previous versions regardless of config,
201
+ // so treat any existing config without an explicit border setting as having borders on.
202
+ if (processedConfig.visual && processedConfig.visual.border === false) {
203
+ processedConfig.visual.border = true
204
+ }
205
+
173
206
  updateConfig({ ...defaults, ...processedConfig })
174
207
  dispatch({ type: 'SET_LOADING', payload: false })
175
208
  }
@@ -184,7 +217,9 @@ const CdcDataBite = (props: CdcDataBiteProps) => {
184
217
  isEditor,
185
218
  showNoDataMessage: false,
186
219
  allowHideSection: false,
187
- filters: config.filters || []
220
+ filters: config.filters || [],
221
+ locale: config.locale,
222
+ dataMetadata: config.dataMetadata
188
223
  })
189
224
 
190
225
  return result.processedContent
@@ -313,8 +348,7 @@ const CdcDataBite = (props: CdcDataBiteProps) => {
313
348
  if (Number.isNaN(value) || typeof value === 'number') {
314
349
  value = String(value)
315
350
  }
316
- const language = 'en-US'
317
- let formattedValue = parseFloat(value).toLocaleString(language, {
351
+ let formattedValue = parseFloat(value).toLocaleString(config.locale, {
318
352
  useGrouping: true,
319
353
  maximumFractionDigits: 6
320
354
  })
@@ -342,6 +376,23 @@ const CdcDataBite = (props: CdcDataBiteProps) => {
342
376
  }
343
377
  })
344
378
 
379
+ if (dataFunction === DATA_FUNCTION_PASSTHROUGH) {
380
+ const sourceData = filteredData.length ? filteredData : config.data
381
+ if (sourceData && sourceData.length > 0) {
382
+ const rawValue = sourceData[0][dataColumn]
383
+ dataBite = rawValue !== undefined && rawValue !== null ? String(rawValue) : ''
384
+ }
385
+
386
+ if (typeof numberFromString(dataBite) === 'number') {
387
+ dataBite = applyPrecision(dataBite)
388
+ if (config.dataFormat.commas) {
389
+ dataBite = applyLocaleString(dataBite)
390
+ }
391
+ }
392
+
393
+ return includePrefixSuffix ? dataFormat.prefix + dataBite + dataFormat.suffix : dataBite
394
+ }
395
+
345
396
  let numericalData = []
346
397
  // Get the column's data
347
398
  if (filteredData.length) {
@@ -443,6 +494,9 @@ const CdcDataBite = (props: CdcDataBiteProps) => {
443
494
  }
444
495
 
445
496
  let body = <Loading />
497
+ const isCompactStyle = config.general?.isCompactStyle ?? false
498
+ const bodySubtext =
499
+ subtext && !isCompactStyle ? <p className='bite-subtext mt-0'>{parse(processContentWithMarkup(subtext))}</p> : null
446
500
 
447
501
  const DataImage = useCallback(() => {
448
502
  let operators = {
@@ -594,120 +648,118 @@ const CdcDataBite = (props: CdcDataBiteProps) => {
594
648
  }
595
649
 
596
650
  const showBite = undefined !== dataColumn && undefined !== dataFunction
651
+ const isTp5 = showBite && biteStyle === 'tp5'
652
+ const bodyClasses = [
653
+ ...innerContainerClasses,
654
+ ...contentClasses,
655
+ isTp5 ? 'bite__style--tp5' : '',
656
+ isTp5 && config.visual?.whiteBackground ? 'white-background-style' : '',
657
+ isTp5 && config.visual?.whiteBackground && config.visual?.border ? 'display-border' : '',
658
+ isTp5 && config.visual?.useWrap ? 'use-wrap' : '',
659
+ !config.visual?.border ? 'no-borders' : ''
660
+ ]
661
+ .filter(Boolean)
662
+ .join(' ')
597
663
  body = (
598
664
  <>
599
- {isEditor && <EditorPanel />}
600
- <Layout.Responsive isEditor={isEditor}>
601
- <div
602
- className={`cove-component__content ${showBite && 'tp5' === biteStyle ? 'bite__style--tp5' : ''} ${
603
- showBite && 'tp5' === biteStyle && config.visual?.whiteBackground ? 'white-background-style' : ''
604
- } ${
605
- showBite && 'tp5' === biteStyle && config.visual?.whiteBackground && config.visual?.border
606
- ? 'display-border'
607
- : ''
608
- }`}
609
- >
610
- {!config.newViz && config.runtime && config.runtime.editorErrorMessage && <Error />}
611
- {(!config.dataColumn || !config.dataFunction) && <Confirm />}
612
- {showBite && biteStyle === 'tp5' ? (
613
- <div
614
- className={`bite-content cdc-callout d-flex flex-column h-100 ${
615
- !config.visual?.whiteBackground ? 'dfe-block cdc-callout--data' : ''
616
- }`}
617
- >
618
- {/* Icon shows by default, hidden when white background is enabled */}
619
- {!config.visual?.whiteBackground && (
620
- <img src={CalloutFlag} alt='' className='cdc-callout__flag' aria-hidden='true' />
621
- )}
622
-
623
- {config.visual?.showTitle && title && title.trim() && (
624
- <h3 className='cdc-callout__heading fw-bold flex-shrink-0 d-flex align-items-start'>
625
- <span>{parse(processContentWithMarkup(title))}</span>
626
- </h3>
627
- )}
628
- <div className='cdc-callout__body d-flex flex-row align-content-start flex-grow-1'>
629
- {showBite && (
630
- <div className='cdc-callout__databite flex-shrink-0 me-3'>{calculateDataBite(true)}</div>
665
+ <VisualizationContent
666
+ bodyClassName={bodyClasses}
667
+ header={
668
+ !isTp5 ? (
669
+ <Title
670
+ showTitle={config.visual?.showTitle}
671
+ titleStyle='legacy'
672
+ config={config}
673
+ title={processContentWithMarkup(title)}
674
+ isDashboard={isDashboard}
675
+ classes={['bite-header', `${config.theme}`]}
676
+ />
677
+ ) : null
678
+ }
679
+ footer={link && link}
680
+ >
681
+ {!config.newViz && config.runtime && config.runtime.editorErrorMessage && <Error />}
682
+ {(!config.dataColumn || !config.dataFunction) && <Confirm />}
683
+ {showBite && biteStyle === 'tp5' ? (
684
+ <div
685
+ className={`bite-content cdc-callout d-flex flex-column h-100 ${
686
+ !config.visual?.whiteBackground ? 'dfe-block cdc-callout--data' : ''
687
+ }`}
688
+ >
689
+ {!config.visual?.whiteBackground && (
690
+ <img src={CalloutFlag} alt='' className='cdc-callout__flag' aria-hidden='true' />
691
+ )}
692
+
693
+ {config.visual?.showTitle && title && title.trim() && (
694
+ <h3 className='cdc-callout__heading fw-bold flex-shrink-0 d-flex align-items-start'>
695
+ <span>{parse(processContentWithMarkup(title))}</span>
696
+ </h3>
697
+ )}
698
+ <div className='cdc-callout__body d-flex flex-row align-content-start flex-grow-1'>
699
+ {showBite && <div className='cdc-callout__databite flex-shrink-0 me-3'>{calculateDataBite(true)}</div>}
700
+ <div className='cdc-callout__content flex-grow-1 d-flex flex-column min-w-0'>
701
+ <p className='mb-0'>{parse(processContentWithMarkup(biteBody))}</p>
702
+ {subtext && !isCompactStyle && (
703
+ <p className='bite-subtext fst-italic flex-shrink-0'>{parse(processContentWithMarkup(subtext))}</p>
631
704
  )}
632
- <div className='cdc-callout__content flex-grow-1 d-flex flex-column min-w-0'>
633
- <p className='mb-0'>{parse(processContentWithMarkup(biteBody))}</p>
634
- {subtext && !config.general.isCompactStyle && (
635
- <p className='bite-subtext fst-italic flex-shrink-0 mt-3'>
636
- {parse(processContentWithMarkup(subtext))}
637
- </p>
638
- )}
639
- </div>
640
705
  </div>
641
706
  </div>
642
- ) : (
643
- <>
644
- <Title
645
- showTitle={config.visual?.showTitle}
646
- titleStyle='legacy'
647
- config={config}
648
- title={processContentWithMarkup(title)}
649
- isDashboard={isDashboard}
650
- classes={['bite-header', `${config.theme}`]}
651
- />
652
- <div className={`bite ${biteClasses.join(' ')}`}>
653
- <div className={`bite-content-container ${contentClasses.join(' ')}`}>
654
- {showBite && 'graphic' === biteStyle && isTop && (
655
- <CircleCallout
656
- theme={config.theme}
657
- text={calculateDataBite()}
658
- biteFontSize={biteFontSize}
659
- dataFormat={dataFormat}
660
- />
661
- )}
662
- {isTop && <DataImage />}
663
- <div className={`bite-content`}>
664
- {showBite && 'title' === biteStyle && (
665
- <div className='bite-value' style={{ fontSize: biteFontSize + 'px' }}>
666
- {calculateDataBite()}
667
- </div>
668
- )}
669
- {showBite && 'split' === biteStyle && (
670
- <div className='bite-value' style={{ fontSize: biteFontSize + 'px' }}>
707
+ </div>
708
+ ) : (
709
+ <div className={`bite ${biteClasses.join(' ')}`}>
710
+ <div className='bite-content-container'>
711
+ {showBite && 'graphic' === biteStyle && isTop && (
712
+ <CircleCallout
713
+ theme={config.theme}
714
+ text={calculateDataBite()}
715
+ biteFontSize={biteFontSize}
716
+ dataFormat={dataFormat}
717
+ />
718
+ )}
719
+ {isTop && <DataImage />}
720
+ <div className={`bite-content`}>
721
+ {showBite && 'title' === biteStyle && (
722
+ <div className='bite-value' style={{ fontSize: biteFontSize + 'px' }}>
723
+ {calculateDataBite()}
724
+ </div>
725
+ )}
726
+ {showBite && 'split' === biteStyle && (
727
+ <div className='bite-value' style={{ fontSize: biteFontSize + 'px' }}>
728
+ {calculateDataBite()}
729
+ </div>
730
+ )}
731
+ <Fragment>
732
+ <div className='bite-content__text-wrap'>
733
+ <p className='bite-text'>
734
+ {showBite && 'body' === biteStyle && (
735
+ <span className='bite-value data-bite-body' style={{ fontSize: biteFontSize + 'px' }}>
736
+ {calculateDataBite()}
737
+ </span>
738
+ )}
739
+ {parse(processContentWithMarkup(biteBody))}
740
+ </p>
741
+ {showBite && 'end' === biteStyle && (
742
+ <span className='bite-value data-bite-body' style={{ fontSize: biteFontSize + 'px' }}>
671
743
  {calculateDataBite()}
672
- </div>
744
+ </span>
673
745
  )}
674
- <Fragment>
675
- <div className='bite-content__text-wrap'>
676
- <p className='bite-text'>
677
- {showBite && 'body' === biteStyle && (
678
- <span className='bite-value data-bite-body' style={{ fontSize: biteFontSize + 'px' }}>
679
- {calculateDataBite()}
680
- </span>
681
- )}
682
- {parse(processContentWithMarkup(biteBody))}
683
- </p>
684
- {showBite && 'end' === biteStyle && (
685
- <span className='bite-value data-bite-body' style={{ fontSize: biteFontSize + 'px' }}>
686
- {calculateDataBite()}
687
- </span>
688
- )}
689
- {subtext && !config.general.isCompactStyle && (
690
- <p className='bite-subtext'>{parse(processContentWithMarkup(subtext))}</p>
691
- )}
692
- </div>
693
- </Fragment>
746
+ {bodySubtext}
694
747
  </div>
695
- {isBottom && <DataImage />}
696
- {showBite && 'graphic' === biteStyle && !isTop && (
697
- <CircleCallout
698
- theme={config.theme}
699
- text={calculateDataBite()}
700
- biteFontSize={biteFontSize}
701
- dataFormat={dataFormat}
702
- />
703
- )}
704
- </div>
748
+ </Fragment>
705
749
  </div>
706
- </>
707
- )}
708
- </div>
709
- {link && link}
710
- </Layout.Responsive>
750
+ {isBottom && <DataImage />}
751
+ {showBite && 'graphic' === biteStyle && !isTop && (
752
+ <CircleCallout
753
+ theme={config.theme}
754
+ text={calculateDataBite()}
755
+ biteFontSize={biteFontSize}
756
+ dataFormat={dataFormat}
757
+ />
758
+ )}
759
+ </div>
760
+ </div>
761
+ )}
762
+ </VisualizationContent>
711
763
  </>
712
764
  )
713
765
  }
@@ -717,29 +769,36 @@ const CdcDataBite = (props: CdcDataBiteProps) => {
717
769
  value={{ config, updateConfig, loading, data: config.data, setParentConfig, isDashboard, isEditor }}
718
770
  >
719
771
  {biteStyle !== 'gradient' && (
720
- <Layout.VisualizationWrapper
772
+ <VisualizationContainer
721
773
  ref={outerContainerRef}
722
774
  config={config}
723
775
  isEditor={isEditor}
724
- showEditorPanel={config?.showEditorPanel}
776
+ currentViewport={currentViewport}
777
+ editorPanel={<EditorPanel />}
725
778
  >
726
779
  {body}
727
- </Layout.VisualizationWrapper>
780
+ </VisualizationContainer>
728
781
  )}
729
782
  {'gradient' === biteStyle && (
730
- <Layout.VisualizationWrapper
783
+ <VisualizationContainer
731
784
  ref={outerContainerRef}
732
785
  config={config}
733
786
  isEditor={isEditor}
734
- showEditorPanel={config?.showEditorPanel}
787
+ currentViewport={currentViewport}
788
+ editorPanel={<EditorPanel />}
735
789
  >
736
- {isEditor && <EditorPanel />}
737
- <Layout.Responsive isEditor={isEditor}>
790
+ <VisualizationContent
791
+ bodyClassName={[...innerContainerClasses, ...contentClasses, 'bite__style--gradient']
792
+ .filter(Boolean)
793
+ .join(' ')}
794
+ footer={link && link}
795
+ subtext={bodySubtext}
796
+ >
738
797
  {!config.newViz && config.runtime && config.runtime.editorErrorMessage && <Error />}
739
798
  {(!config.dataColumn || !config.dataFunction) && <Confirm />}
740
799
  <GradientBite label={config.title} value={calculateDataBite()} />
741
- </Layout.Responsive>
742
- </Layout.VisualizationWrapper>
800
+ </VisualizationContent>
801
+ </VisualizationContainer>
743
802
  )}
744
803
  </Context.Provider>
745
804
  )
@@ -100,7 +100,7 @@ export const GeneralSectionTests: Story = {
100
100
  return {
101
101
  hasSvg: !!svg,
102
102
  textCount: textElements.length,
103
- containerClasses: canvasElement.querySelector('.cdc-open-viz-module')?.className || ''
103
+ containerClasses: canvasElement.querySelector('.cove-visualization')?.className || ''
104
104
  }
105
105
  }
106
106
 
@@ -145,7 +145,7 @@ export const GeneralSectionTests: Story = {
145
145
 
146
146
  await performAndAssert(
147
147
  'Title Update',
148
- () => canvasElement.querySelector('.cove-component__header')?.textContent?.trim() || '',
148
+ () => canvasElement.querySelector('.cove-visualization__header')?.textContent?.trim() || '',
149
149
  async () => {}, // action already performed above
150
150
  (before, after) => after === 'Updated Data Bite Title'
151
151
  )
@@ -158,7 +158,7 @@ export const GeneralSectionTests: Story = {
158
158
  expect(showTitleCheckbox).toBeTruthy()
159
159
 
160
160
  const getTitleVisibility = () => {
161
- const titleElement = canvasElement.querySelector('.cove-component__header') as HTMLElement
161
+ const titleElement = canvasElement.querySelector('.cove-visualization__header') as HTMLElement
162
162
  return titleElement && titleElement.offsetParent !== null
163
163
  }
164
164
 
@@ -702,7 +702,7 @@ export const VisualSectionTests: Story = {
702
702
  // TEST 3: Display Border Toggle
703
703
  // Expectation: Border styling changes when toggled (classes or computed styles)
704
704
  // ============================================================================
705
- const contentContainer = () => canvasElement.querySelector('.cove-component__content') as HTMLElement
705
+ const contentContainer = () => canvasElement.querySelector('.cove-visualization__body') as HTMLElement
706
706
  expect(contentContainer()).toBeTruthy()
707
707
 
708
708
  // Note: Border checkbox uses name="border", other checkboxes use similar simple names
@@ -736,7 +736,7 @@ export const VisualSectionTests: Story = {
736
736
 
737
737
  // Test border checkbox with comprehensive boolean testing AND visual validation
738
738
  const getBorderVisualState = () => {
739
- const element = canvasElement.querySelector('.cove-component__content')
739
+ const element = canvasElement.querySelector('.cove-visualization__body')
740
740
  return {
741
741
  classes: Array.from(element!.classList).sort().join(' '),
742
742
  hasNoBordersClass: element!.classList.contains('no-borders'),
@@ -792,14 +792,14 @@ export const VisualSectionTests: Story = {
792
792
 
793
793
  // Test remaining checkboxes with comprehensive boolean testing AND visual validation
794
794
  const getGeneralVisualState = () => {
795
- const element = canvasElement.querySelector('.cove-component__content')
795
+ const element = canvasElement.querySelector('.cove-visualization__body')
796
796
  return {
797
797
  classes: Array.from(element!.classList).sort().join(' '),
798
798
  // Check for specific component classes that these controls add
799
799
  hasAccentClass: element!.classList.contains('component--has-accent'),
800
800
  hasBackgroundClass: element!.classList.contains('component--has-background'),
801
- hasBorderColorThemeClass: element!.classList.contains('component--has-borderColorTheme'),
802
- hideBackgroundColorClass: element!.classList.contains('component--hideBackgroundColor'),
801
+ hasBorderColorThemeClass: element!.classList.contains('component--has-border-color-theme'),
802
+ hideBackgroundColorClass: element!.classList.contains('component--hide-background-color'),
803
803
  themeClass: Array.from(element!.classList).find(cls => cls.includes('theme-')) || 'no-theme',
804
804
  backgroundStyle: getComputedStyle(element!).backgroundColor
805
805
  }
@@ -1,6 +1,7 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
2
  import DataBite from '../CdcDataBite'
3
- import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
3
+ import { assertVisualizationRendered, waitForPresence } from '@cdc/core/helpers/testing'
4
+ import { expect } from 'storybook/test'
4
5
 
5
6
  const meta: Meta<typeof DataBite> = {
6
7
  title: 'Components/Templates/Data Bite',
@@ -89,6 +90,17 @@ export const Data_Bite_TP5_White_Background: Story = {
89
90
  }
90
91
  }
91
92
 
93
+ export const Data_Bite_With_Metadata: Story = {
94
+ args: {
95
+ configUrl: '/packages/data-bite/tests/fixtures/data-bite-config-with-metadata.json'
96
+ },
97
+ play: async ({ canvasElement }) => {
98
+ await assertVisualizationRendered(canvasElement)
99
+ const subtext = await waitForPresence('.bite-subtext', canvasElement)
100
+ expect(subtext?.textContent).toContain('January 15, 2026')
101
+ }
102
+ }
103
+
92
104
  // Simple editor mode story for basic rendering
93
105
  export const Editor_Mode_Basic: Story = {
94
106
  args: {