@cdc/waffle-chart 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.
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useReducer, useState } from 'react'
1
+ import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'
2
2
 
3
3
  // visx
4
4
  import { Circle, Bar } from '@visx/shape'
@@ -13,6 +13,7 @@ import ResizeObserver from 'resize-observer-polyfill'
13
13
  import { Config } from './types/Config'
14
14
  import getViewport from '@cdc/core/helpers/getViewport'
15
15
  import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
16
+ import { DATA_OPERATORS } from '@cdc/core/helpers/constants'
16
17
 
17
18
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
18
19
  import Loading from '@cdc/core/components/Loading'
@@ -29,6 +30,13 @@ import './scss/main.scss'
29
30
  import Title from '@cdc/core/components/ui/Title'
30
31
  import Layout from '@cdc/core/components/Layout'
31
32
 
33
+ // images
34
+ import CalloutFlag from './images/callout-flag.svg?url'
35
+
36
+ // TP5 Style Constants
37
+ const TP5_NODE_WIDTH = 13
38
+ const TP5_NODE_SPACER = 3
39
+
32
40
  type CdcWaffleChartProps = {
33
41
  configUrl?: string
34
42
  config?: Config
@@ -67,7 +75,7 @@ const WaffleChart = ({ config, isEditor, link = '', showConfigConfirm, updateCon
67
75
  const gaugeColor = config.visual.colors[config.theme]
68
76
  let dataFontSize = config.fontSize ? { fontSize: config.fontSize + 'px' } : null
69
77
 
70
- const calculateData = useCallback(() => {
78
+ const [dataPercentage, waffleDenominator, waffleNumerator] = useMemo(() => {
71
79
  //If either the column or function aren't set, do not calculate
72
80
  if (!dataColumn || !dataFunction) {
73
81
  return ''
@@ -239,26 +247,26 @@ const WaffleChart = ({ config, isEditor, link = '', showConfigConfirm, updateCon
239
247
  applyPrecision(waffleNumerator)
240
248
  ]
241
249
  }, [
242
- dataColumn,
243
- dataFunction,
244
250
  config.data,
245
251
  filters,
252
+ dataColumn,
253
+ dataFunction,
246
254
  dataConditionalColumn,
247
255
  dataConditionalOperator,
248
256
  dataConditionalComparate,
249
257
  customDenom,
258
+ dataDenom,
250
259
  dataDenomColumn,
251
260
  dataDenomFunction,
252
- roundToPlace,
253
- dataDenom
261
+ roundToPlace
254
262
  ])
255
263
 
256
- const [dataPercentage, waffleDenominator, waffleNumerator] = calculateData()
257
-
258
264
  const buildWaffle = useCallback(() => {
259
265
  let waffleData = []
260
- let nodeWidthNum = parseInt(nodeWidth, 10)
261
- let nodeSpacerNum = parseInt(nodeSpacer, 10)
266
+ // Use standardized values for TP5 style
267
+ const isTP5 = config.visualizationType === 'TP5 Waffle'
268
+ let nodeWidthNum = isTP5 ? TP5_NODE_WIDTH : parseInt(nodeWidth, 10)
269
+ let nodeSpacerNum = isTP5 ? TP5_NODE_SPACER : parseInt(nodeSpacer, 10)
262
270
 
263
271
  const calculatePos = (shape, axis, index, width, spacer) => {
264
272
  let mod = axis === 'x' ? index % 10 : axis === 'y' ? Math.floor(index / 10) : null
@@ -271,7 +279,8 @@ const WaffleChart = ({ config, isEditor, link = '', showConfigConfirm, updateCon
271
279
  x: calculatePos(shape, 'x', i, nodeWidthNum, nodeSpacerNum),
272
280
  y: calculatePos(shape, 'y', i, nodeWidthNum, nodeSpacerNum),
273
281
  color: config.visual.colors[theme],
274
- opacity: i + 1 > 100 - Math.round(dataPercentage) ? 1 : 0.35
282
+ opacity: i + 1 > 100 - Math.round(dataPercentage) ? 1 : 0.2,
283
+ isFilled: i + 1 > 100 - Math.round(dataPercentage)
275
284
  }
276
285
  waffleData.push(newNode)
277
286
  }
@@ -281,25 +290,37 @@ const WaffleChart = ({ config, isEditor, link = '', showConfigConfirm, updateCon
281
290
  <Bar
282
291
  className='cdc-waffle-chart__node'
283
292
  style={{ transitionDelay: `${0.1 * key}ms` }}
284
- x={node.x}
285
- y={node.y}
286
- width={nodeWidthNum}
287
- height={nodeWidthNum}
288
- fill={node.color}
289
- fillOpacity={node.opacity}
293
+ x={isTP5 && !node.isFilled ? node.x + 1 : node.x}
294
+ y={isTP5 && !node.isFilled ? node.y + 1 : node.y}
295
+ width={isTP5 && !node.isFilled ? nodeWidthNum - 2 : nodeWidthNum}
296
+ height={isTP5 && !node.isFilled ? nodeWidthNum - 2 : nodeWidthNum}
297
+ fill={isTP5 ? (node.isFilled ? '#009EC1' : '#DFF2F6') : node.color}
298
+ fillOpacity={isTP5 ? 1 : node.opacity}
299
+ stroke={isTP5 ? (!node.isFilled ? '#009EC1' : undefined) : undefined}
300
+ strokeWidth={isTP5 ? (!node.isFilled ? 1 : 0) : 0}
290
301
  key={key}
291
302
  />
292
303
  ) : node.shape === 'person' ? (
293
304
  <path
305
+ className='cdc-waffle-chart__node'
294
306
  style={{
295
- transform: `translateX(${node.x + nodeWidthNum / 4}px) translateY(${node.y}px) scale(${nodeWidthNum / 20})`
307
+ transform: isTP5
308
+ ? `translateX(${node.x}px) translateY(${node.y + nodeWidthNum * 0.1}px) scale(${
309
+ (nodeWidthNum * 0.8) / 448
310
+ })`
311
+ : `translateX(${node.x + nodeWidthNum / 4}px) translateY(${node.y}px) scale(${nodeWidthNum / 20})`,
312
+ transitionDelay: `${0.1 * key}ms`
296
313
  }}
297
- fill={node.color}
298
- fillOpacity={node.opacity}
314
+ fill={isTP5 ? (node.isFilled ? '#009EC1' : 'transparent') : node.color}
315
+ fillOpacity={isTP5 ? 1 : node.opacity}
316
+ stroke={isTP5 ? (!node.isFilled ? '#009EC1' : undefined) : undefined}
317
+ strokeWidth={isTP5 ? (!node.isFilled ? 448 / nodeWidthNum : 0) : 0}
299
318
  key={key}
300
- d='M3.75,0a2.5,2.5,0,1,1-2.5,2.5A2.5,2.5,0,0,1,3.75,0M5.625,5.625H5.18125a3.433,3.433,0,0,1-2.8625,0H1.875A1.875,1.875,
301
- 0,0,0,0,7.5v5.3125a.9375.9375,0,0,0,.9375.9375h.625v5.3125A.9375.9375,0,0,0,2.5,20H5a.9375.9375,0,0,0,
302
- .9375-.9375V13.75h.625A.9375.9375,0,0,0,7.5,12.8125V7.5A1.875,1.875,0,0,0,5.625,5.625Z'
319
+ d={
320
+ isTP5
321
+ ? 'M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512l388.6 0c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304l-91.4 0z'
322
+ : 'M3.75,0a2.5,2.5,0,1,1-2.5,2.5A2.5,2.5,0,0,1,3.75,0M5.625,5.625H5.18125a3.433,3.433,0,0,1-2.8625,0H1.875A1.875,1.875,0,0,0,0,7.5v5.3125a.9375.9375,0,0,0,.9375.9375h.625v5.3125A.9375.9375,0,0,0,2.5,20H5a.9375.9375,0,0,0,.9375-.9375V13.75h.625A.9375.9375,0,0,0,7.5,12.8125V7.5A1.875,1.875,0,0,0,5.625,5.625Z'
323
+ }
303
324
  ></path>
304
325
  ) : (
305
326
  <Circle
@@ -307,18 +328,28 @@ const WaffleChart = ({ config, isEditor, link = '', showConfigConfirm, updateCon
307
328
  style={{ transitionDelay: `${0.1 * key}ms` }}
308
329
  cx={node.x}
309
330
  cy={node.y}
310
- r={nodeWidthNum / 2}
311
- fill={node.color}
312
- fillOpacity={node.opacity}
331
+ r={isTP5 && !node.isFilled ? nodeWidthNum / 2 - 1 : nodeWidthNum / 2}
332
+ fill={isTP5 ? (node.isFilled ? '#009EC1' : '#DFF2F6') : node.color}
333
+ fillOpacity={isTP5 ? 1 : node.opacity}
334
+ stroke={isTP5 ? (!node.isFilled ? '#009EC1' : undefined) : undefined}
335
+ strokeWidth={isTP5 ? (!node.isFilled ? 1 : 0) : 0}
313
336
  key={key}
314
337
  />
315
338
  )
316
339
  )
317
- }, [theme, dataPercentage, shape, nodeWidth, nodeSpacer])
340
+ }, [theme, dataPercentage, shape, nodeWidth, nodeSpacer, config.visualizationType, config.visual?.whiteBackground])
318
341
 
319
342
  const setRatio = useCallback(() => {
320
- return nodeWidth * 10 + nodeSpacer * 9
321
- }, [nodeWidth, nodeSpacer])
343
+ const isTP5 = config.visualizationType === 'TP5 Waffle'
344
+ const width = isTP5 ? TP5_NODE_WIDTH : nodeWidth
345
+ const spacer = isTP5 ? TP5_NODE_SPACER : nodeSpacer
346
+ return width * 10 + spacer * 9
347
+ }, [nodeWidth, nodeSpacer, config.visualizationType])
348
+
349
+ const setSvgSize = useCallback(() => {
350
+ // Add 2px padding to account for strokes on edges
351
+ return setRatio() + 2
352
+ }, [nodeWidth, nodeSpacer, config.visualizationType])
322
353
 
323
354
  const { innerContainerClasses, contentClasses } = useDataVizClasses(config)
324
355
 
@@ -359,76 +390,108 @@ const WaffleChart = ({ config, isEditor, link = '', showConfigConfirm, updateCon
359
390
  )
360
391
  }
361
392
 
393
+ // Render waffle chart content (without title)
394
+ const renderChartContent = () => (
395
+ <>
396
+ {!config.newViz && config.runtime && config.runtime.editorErrorMessage && (
397
+ <Error updateConfig={updateConfig} config={config} />
398
+ )}
399
+ {config.newViz && showConfigConfirm && <Confirm updateConfig={updateConfig} config={config} />}
400
+ <div className='cove-component__content-wrap p-0'>
401
+ {config.visualizationType === 'Gauge' && (
402
+ <div className={`cove-gauge-chart${config.overallFontSize ? ' font-' + config.overallFontSize : ''}`}>
403
+ <div className='cove-gauge-chart__chart'>
404
+ <div className='cove-waffle-chart__data--primary' style={dataFontSize}>
405
+ {prefix ? prefix : ' '}
406
+ {config.showPercent ? dataPercentage : waffleNumerator}
407
+ {suffix ? suffix + ' ' : ' '} {config.valueDescription}{' '}
408
+ {config.showDenominator && waffleDenominator ? waffleDenominator : ' '}
409
+ </div>
410
+ <div className='cove-waffle-chart__data--text'>{parse(content)}</div>
411
+ <svg height={config.gauge.height + 4} width={'100%'} style={{ overflow: 'visible' }}>
412
+ <Group top={2} left={2}>
413
+ <foreignObject
414
+ style={{ border: '1px solid black' }}
415
+ x={0}
416
+ y={0}
417
+ width={config.gauge.width}
418
+ height={config.gauge.height}
419
+ fill='#fff'
420
+ />
421
+ <Bar x={0} y={0} width={xScale(waffleNumerator)} height={config.gauge.height} fill={gaugeColor} />
422
+ </Group>
423
+ </svg>
424
+ <div className={'cove-waffle-chart__subtext subtext'}>{parse(subtext)}</div>
425
+ </div>
426
+ </div>
427
+ )}
428
+ {config.visualizationType !== 'Gauge' && (
429
+ <div
430
+ className={`cove-waffle-chart${orientation === 'vertical' ? ' cove-waffle-chart--verical' : ''}${
431
+ config.overallFontSize ? ' font-' + config.overallFontSize : ''
432
+ }`}
433
+ >
434
+ <div className='cove-waffle-chart__chart' style={{ width: setRatio() }}>
435
+ <svg width={setSvgSize()} height={setSvgSize()} style={{ display: 'block' }}>
436
+ <Group top={1} left={1}>
437
+ {buildWaffle()}
438
+ </Group>
439
+ </svg>
440
+ </div>
441
+ {(dataPercentage || content) && (
442
+ <div className='cove-waffle-chart__data'>
443
+ {dataPercentage && (
444
+ <div className='cove-waffle-chart__data--primary' style={dataFontSize}>
445
+ {prefix ? prefix : null}
446
+ {dataPercentage}
447
+ {suffix ? suffix : null}
448
+ </div>
449
+ )}
450
+ <div className='cove-waffle-chart__data--text'>{parse(content)}</div>
451
+
452
+ {subtext && <div className='cove-waffle-chart__subtext subtext fst-italic'>{parse(subtext)}</div>}
453
+ </div>
454
+ )}
455
+ </div>
456
+ )}
457
+ </div>
458
+ </>
459
+ )
460
+
461
+ // TP5 Style: render with callout wrapper inside cove-component__content
462
+ if (config.visualizationType === 'TP5 Waffle') {
463
+ const calloutClasses = ['cdc-callout', 'd-flex', 'flex-column']
464
+ if (!config.visual?.whiteBackground) {
465
+ calloutClasses.push('dfe-block', 'cdc-callout--data')
466
+ }
467
+
468
+ return (
469
+ <div className='cove-component__content p-0 border-0'>
470
+ <div className={calloutClasses.join(' ')}>
471
+ {!config.visual?.whiteBackground && (
472
+ <img src={CalloutFlag} alt='' className='cdc-callout__flag' aria-hidden='true' />
473
+ )}
474
+ {config.showTitle && title && title.trim() && (
475
+ <h3 className='cdc-callout__heading fw-bold flex-shrink-0'>{parse(title)}</h3>
476
+ )}
477
+ <div className='w-100 mw-100 overflow-hidden'>{renderChartContent()}</div>
478
+ </div>
479
+ {link && link}
480
+ </div>
481
+ )
482
+ }
483
+
484
+ // Original Style: Regular title and content
362
485
  return (
363
486
  <div className='cove-component__content'>
364
487
  <Title
365
488
  showTitle={config.showTitle}
366
489
  title={title}
490
+ titleStyle='legacy'
367
491
  config={config}
368
492
  classes={['chart-title', `${config.theme}`, 'mb-0']}
369
493
  />
370
- <div className={contentClasses.join(' ')}>
371
- {!config.newViz && config.runtime && config.runtime.editorErrorMessage && (
372
- <Error updateConfig={updateConfig} config={config} />
373
- )}
374
- {config.newViz && showConfigConfirm && <Confirm updateConfig={updateConfig} config={config} />}
375
- <div className='cove-component__content-wrap'>
376
- {config.visualizationType === 'Gauge' && (
377
- <div className={`cove-gauge-chart${config.overallFontSize ? ' font-' + config.overallFontSize : ''}`}>
378
- <div className='cove-gauge-chart__chart'>
379
- <div className='cove-waffle-chart__data--primary' style={dataFontSize}>
380
- {prefix ? prefix : ' '}
381
- {config.showPercent ? dataPercentage : waffleNumerator}
382
- {suffix ? suffix + ' ' : ' '} {config.valueDescription}{' '}
383
- {config.showDenominator && waffleDenominator ? waffleDenominator : ' '}
384
- </div>
385
- <div className='cove-waffle-chart__data--text'>{parse(content)}</div>
386
- <svg height={config.gauge.height} width={'100%'}>
387
- <Group>
388
- <foreignObject
389
- style={{ border: '1px solid black' }}
390
- x={0}
391
- y={0}
392
- width={config.gauge.width}
393
- height={config.gauge.height}
394
- fill='#fff'
395
- />
396
- <Bar x={0} y={0} width={xScale(waffleNumerator)} height={config.gauge.height} fill={gaugeColor} />
397
- </Group>
398
- </svg>
399
- <div className={'cove-waffle-chart__subtext subtext'}>{parse(subtext)}</div>
400
- </div>
401
- </div>
402
- )}
403
- {config.visualizationType !== 'Gauge' && (
404
- <div
405
- className={`cove-waffle-chart${orientation === 'vertical' ? ' cove-waffle-chart--verical' : ''}${
406
- config.overallFontSize ? ' font-' + config.overallFontSize : ''
407
- }`}
408
- >
409
- <div className='cove-waffle-chart__chart' style={{ width: setRatio() }}>
410
- <svg width={setRatio()} height={setRatio()}>
411
- <Group>{buildWaffle()}</Group>
412
- </svg>
413
- </div>
414
- {(dataPercentage || content) && (
415
- <div className='cove-waffle-chart__data'>
416
- {dataPercentage && (
417
- <div className='cove-waffle-chart__data--primary' style={dataFontSize}>
418
- {prefix ? prefix : null}
419
- {dataPercentage}
420
- {suffix ? suffix : null}
421
- </div>
422
- )}
423
- <div className='cove-waffle-chart__data--text'>{parse(content)}</div>
424
-
425
- {subtext && <div className='cove-waffle-chart__subtext subtext'>{parse(subtext)}</div>}
426
- </div>
427
- )}
428
- </div>
429
- )}
430
- </div>
431
- </div>
494
+ <div className={contentClasses.join(' ')}>{renderChartContent()}</div>
432
495
  {link && link}
433
496
  </div>
434
497
  )
@@ -522,21 +585,17 @@ const CdcWaffleChart = ({
522
585
  let content = <Loading />
523
586
 
524
587
  if (loading === false) {
525
- let body = (
526
- <Layout.Responsive isEditor={isEditor}>
527
- <WaffleChart
528
- config={config}
529
- isEditor={isEditor}
530
- showConfigConfirm={showConfigConfirm}
531
- updateConfig={updateConfig}
532
- />
533
- </Layout.Responsive>
534
- )
535
-
536
588
  content = (
537
589
  <>
538
- {isEditor && <EditorPanel showConfigConfirm={showConfigConfirm}>{body}</EditorPanel>}
539
- {!isEditor && body}
590
+ {isEditor && <EditorPanel showConfigConfirm={showConfigConfirm} />}
591
+ <Layout.Responsive isEditor={isEditor}>
592
+ <WaffleChart
593
+ config={config}
594
+ isEditor={isEditor}
595
+ showConfigConfirm={showConfigConfirm}
596
+ updateConfig={updateConfig}
597
+ />
598
+ </Layout.Responsive>
540
599
  </>
541
600
  )
542
601
  }
@@ -579,18 +638,5 @@ export const DATA_FUNCTIONS = [
579
638
  DATA_FUNCTION_SUM
580
639
  ]
581
640
 
582
- export const DATA_OPERATOR_LESS = '<'
583
- export const DATA_OPERATOR_GREATER = '>'
584
- export const DATA_OPERATOR_LESSEQUAL = '<='
585
- export const DATA_OPERATOR_GREATEREQUAL = '>='
586
- export const DATA_OPERATOR_EQUAL = '='
587
- export const DATA_OPERATOR_NOTEQUAL = '≠'
588
-
589
- export const DATA_OPERATORS = [
590
- DATA_OPERATOR_LESS,
591
- DATA_OPERATOR_GREATER,
592
- DATA_OPERATOR_LESSEQUAL,
593
- DATA_OPERATOR_GREATEREQUAL,
594
- DATA_OPERATOR_EQUAL,
595
- DATA_OPERATOR_NOTEQUAL
596
- ]
641
+ // Re-export DATA_OPERATORS for backward compatibility
642
+ export { DATA_OPERATORS }
@@ -100,8 +100,12 @@ export const GeneralSectionTests: Story = {
100
100
  // TEST 3: Show Title Toggle
101
101
  // Expectation: Header region appears / disappears (DOM visibility change).
102
102
  // ============================================================================
103
- const showTitleCheckbox = canvasElement.querySelector('input[name*="showTitle"]') as HTMLInputElement
104
- const checkboxWrapper = showTitleCheckbox?.closest('.cove-input__checkbox--small')
103
+ // Find show title checkbox by label text
104
+ const showTitleCheckbox = Array.from(canvasElement.querySelectorAll('input[type="checkbox"]')).find(input => {
105
+ const label = input.closest('label')
106
+ return label?.textContent?.includes('show title')
107
+ }) as HTMLInputElement
108
+ const checkboxWrapper = showTitleCheckbox?.closest('label.checkbox')
105
109
  expect(showTitleCheckbox).toBeTruthy()
106
110
  expect(checkboxWrapper).toBeTruthy()
107
111
 
@@ -110,7 +114,7 @@ export const GeneralSectionTests: Story = {
110
114
  'Title Toggle',
111
115
  () => showTitleCheckbox.checked,
112
116
  async () => {
113
- await userEvent.click(checkboxWrapper as HTMLElement)
117
+ await userEvent.click(showTitleCheckbox)
114
118
  },
115
119
  (before, after) => after === !wasChecked
116
120
  )
@@ -128,7 +132,7 @@ export const GeneralSectionTests: Story = {
128
132
  'Title Toggle Reset',
129
133
  () => showTitleCheckbox.checked,
130
134
  async () => {
131
- await userEvent.click(checkboxWrapper as HTMLElement)
135
+ await userEvent.click(showTitleCheckbox)
132
136
  },
133
137
  (before, after) => after === wasChecked
134
138
  )
@@ -253,7 +257,15 @@ export const DataSectionTests: Story = {
253
257
  'Clear Conditional Value',
254
258
  getValueText,
255
259
  async () => {
260
+ // Clear the conditional column to fully reset the filter
261
+ const conditionalColumnSelect = canvasElement.querySelector(
262
+ 'select[name="dataConditionalColumn"]'
263
+ ) as HTMLSelectElement
264
+ await userEvent.selectOptions(conditionalColumnSelect, '')
256
265
  await userEvent.clear(conditionalValueInput)
266
+ conditionalValueInput.blur() // Trigger change event
267
+ // Wait for debounced input processing (TextField uses 500ms debounce)
268
+ await new Promise(resolve => setTimeout(resolve, 600))
257
269
  },
258
270
  (before, after) => after !== before
259
271
  )
@@ -312,7 +324,7 @@ export const DataSectionTests: Story = {
312
324
  'Custom Denominator Toggle',
313
325
  getValueText,
314
326
  async () => {
315
- await userEvent.click(customDenomWrapper)
327
+ await userEvent.click(customDenomCheckbox)
316
328
  },
317
329
  (before, after) => after !== before
318
330
  )
@@ -404,38 +416,12 @@ export const DataSectionTests: Story = {
404
416
  },
405
417
  (before, after) => after !== before && after.endsWith('deaths')
406
418
  )
407
-
408
- // ============================================================================
409
- // TEST 16: Add Filter (state = Alaska)
410
- // Expectation: Primary value text changes after filter applied.
411
- // ============================================================================
412
- const addFilterButton = Array.from(canvasElement.querySelectorAll('button')).find(
413
- b => (b as HTMLButtonElement).textContent?.trim() === 'Add Filter'
414
- ) as HTMLButtonElement
415
- await performAndAssert(
416
- 'Add Filter',
417
- getValueText,
418
- async () => {
419
- await userEvent.click(addFilterButton)
420
-
421
- await waitForPresence('.filters-list .edit-block:last-of-type', canvasElement)
422
-
423
- const newFilter = canvasElement.querySelector('.filters-list .edit-block:last-of-type') as HTMLElement
424
- const [colSelect, valSelect] = Array.from(newFilter.querySelectorAll('select')) as HTMLSelectElement[]
425
- await userEvent.selectOptions(colSelect, 'state')
426
-
427
- await waitForOptionsToPopulate(valSelect)
428
-
429
- await userEvent.selectOptions(valSelect, 'Alaska')
430
- },
431
- (before, after) => after !== before
432
- )
433
419
  }
434
420
  }
435
421
 
436
422
  /**
437
- * VISUAL SECTION TESTS
438
- * Tests all functionality within the Visual accordion
423
+ * CHART SETTINGS AND VISUAL SECTION TESTS
424
+ * Tests functionality within the Chart Settings and Visual accordions
439
425
  */
440
426
  export const VisualSectionTests: Story = {
441
427
  args: {
@@ -445,7 +431,8 @@ export const VisualSectionTests: Story = {
445
431
  play: async ({ canvasElement }) => {
446
432
  const canvas = within(canvasElement)
447
433
  await waitForEditor(canvas)
448
- await openAccordion(canvas, 'Visual')
434
+ // Open Chart Settings accordion first for tests 1-5
435
+ await openAccordion(canvas, 'Chart Settings')
449
436
  // Core helper functions used throughout the visual tests
450
437
  const waffleRoot = () => canvasElement.querySelector('.cove-waffle-chart') as HTMLElement
451
438
  const contentContainer = () => canvasElement.querySelector('.cove-component__content > div') as HTMLElement
@@ -583,6 +570,9 @@ export const VisualSectionTests: Story = {
583
570
  // TEST 6: Overall Font Size Change
584
571
  // Expectation: font-small|font-medium|font-large class changes on waffle root.
585
572
  // ============================================================================
573
+ // Open Visual accordion for tests 6+
574
+ await openAccordion(canvas, 'Visual')
575
+
586
576
  const overallFontClass = () => {
587
577
  const root = waffleRoot()
588
578
  if (!root) return ''
@@ -618,7 +608,7 @@ export const VisualSectionTests: Story = {
618
608
  return node?.getAttribute('fill') || ''
619
609
  }
620
610
 
621
- const themeButtons = Array.from(canvasElement.querySelectorAll('.color-palette li')) as HTMLElement[]
611
+ const themeButtons = Array.from(canvasElement.querySelectorAll('.color-palette button')) as HTMLElement[]
622
612
  expect(themeButtons.length).toBeGreaterThan(1)
623
613
  await performAndAssert(
624
614
  'Theme Change',
@@ -638,9 +628,10 @@ export const VisualSectionTests: Story = {
638
628
  // ============================================================================
639
629
  const contentClassSig = () => Array.from(contentContainer().classList).sort().join(' ')
640
630
 
641
- const borderCheckbox = canvasElement.querySelector('input[name="visual-null-border"]') as HTMLInputElement
631
+ // Find border checkbox by exact label text
632
+ const borderCheckbox = canvas.getByLabelText('Display Border') as HTMLInputElement
642
633
  expect(borderCheckbox).toBeTruthy()
643
- const borderWrapper = borderCheckbox.closest('.cove-input__checkbox--small') as HTMLElement
634
+ const borderWrapper = borderCheckbox.closest('label.checkbox') as HTMLElement
644
635
  expect(borderWrapper).toBeTruthy()
645
636
  const borderStyleSig = () => {
646
637
  const el = contentContainer()
@@ -656,7 +647,7 @@ export const VisualSectionTests: Story = {
656
647
  'Border Toggle',
657
648
  borderStyleSig,
658
649
  async () => {
659
- await userEvent.click(borderWrapper)
650
+ await userEvent.click(borderCheckbox)
660
651
  },
661
652
  (before, after) =>
662
653
  before.classes !== after.classes ||
@@ -669,17 +660,16 @@ export const VisualSectionTests: Story = {
669
660
  // TEST 9: Theme Border Color Toggle
670
661
  // Expectation: Class 'component--has-borderColorTheme' toggles.
671
662
  // ============================================================================
672
- const borderColorThemeCheckbox = canvasElement.querySelector(
673
- 'input[name="visual-null-borderColorTheme"]'
674
- ) as HTMLInputElement
663
+ // Find border color theme checkbox by exact label text
664
+ const borderColorThemeCheckbox = canvas.getByLabelText('Use Border Color Theme') as HTMLInputElement
675
665
  expect(borderColorThemeCheckbox).toBeTruthy()
676
- const borderColorThemeWrapper = borderColorThemeCheckbox.closest('.cove-input__checkbox--small') as HTMLElement
666
+ const borderColorThemeWrapper = borderColorThemeCheckbox.closest('label.checkbox') as HTMLElement
677
667
  expect(borderColorThemeWrapper).toBeTruthy()
678
668
  await performAndAssert(
679
669
  'Border Color Theme Toggle',
680
670
  contentClassSig,
681
671
  async () => {
682
- await userEvent.click(borderColorThemeWrapper)
672
+ await userEvent.click(borderColorThemeCheckbox)
683
673
  },
684
674
  (before, after) => before !== after && (after.includes('borderColorTheme') || before.includes('borderColorTheme'))
685
675
  )
@@ -688,15 +678,16 @@ export const VisualSectionTests: Story = {
688
678
  // TEST 10: Accent Style Toggle
689
679
  // Expectation: Class 'component--has-accent' toggles.
690
680
  // ============================================================================
691
- const accentCheckbox = canvasElement.querySelector('input[name="visual-null-accent"]') as HTMLInputElement
681
+ // Find accent checkbox by exact label text
682
+ const accentCheckbox = canvas.getByLabelText('Use Accent Style') as HTMLInputElement
692
683
  expect(accentCheckbox).toBeTruthy()
693
- const accentWrapper = accentCheckbox.closest('.cove-input__checkbox--small') as HTMLElement
684
+ const accentWrapper = accentCheckbox.closest('label.checkbox') as HTMLElement
694
685
  expect(accentWrapper).toBeTruthy()
695
686
  await performAndAssert(
696
687
  'Accent Toggle',
697
688
  contentClassSig,
698
689
  async () => {
699
- await userEvent.click(accentWrapper)
690
+ await userEvent.click(accentCheckbox)
700
691
  },
701
692
  (before, after) => before !== after
702
693
  )
@@ -705,15 +696,16 @@ export const VisualSectionTests: Story = {
705
696
  // TEST 11: Theme Background Color Toggle
706
697
  // Expectation: Class 'component--has-background' toggles.
707
698
  // ============================================================================
708
- const backgroundCheckbox = canvasElement.querySelector('input[name="visual-null-background"]') as HTMLInputElement
699
+ // Find background checkbox by exact label text
700
+ const backgroundCheckbox = canvas.getByLabelText('Use Theme Background Color') as HTMLInputElement
709
701
  expect(backgroundCheckbox).toBeTruthy()
710
- const backgroundWrapper = backgroundCheckbox.closest('.cove-input__checkbox--small') as HTMLElement
702
+ const backgroundWrapper = backgroundCheckbox.closest('label.checkbox') as HTMLElement
711
703
  expect(backgroundWrapper).toBeTruthy()
712
704
  await performAndAssert(
713
705
  'Background Toggle',
714
706
  contentClassSig,
715
707
  async () => {
716
- await userEvent.click(backgroundWrapper)
708
+ await userEvent.click(backgroundCheckbox)
717
709
  },
718
710
  (before, after) => before !== after
719
711
  )
@@ -722,17 +714,16 @@ export const VisualSectionTests: Story = {
722
714
  // TEST 12: Hide Background Color Toggle
723
715
  // Expectation: Class 'component--hideBackgroundColor' toggles.
724
716
  // ============================================================================
725
- const hideBackgroundCheckbox = canvasElement.querySelector(
726
- 'input[name="visual-null-hideBackgroundColor"]'
727
- ) as HTMLInputElement
717
+ // Find hide background checkbox by exact label text
718
+ const hideBackgroundCheckbox = canvas.getByLabelText('Hide Background Color') as HTMLInputElement
728
719
  expect(hideBackgroundCheckbox).toBeTruthy()
729
- const hideBackgroundWrapper = hideBackgroundCheckbox.closest('.cove-input__checkbox--small') as HTMLElement
720
+ const hideBackgroundWrapper = hideBackgroundCheckbox.closest('label.checkbox') as HTMLElement
730
721
  expect(hideBackgroundWrapper).toBeTruthy()
731
722
  await performAndAssert(
732
723
  'Hide Background Toggle',
733
724
  contentClassSig,
734
725
  async () => {
735
- await userEvent.click(hideBackgroundWrapper)
726
+ await userEvent.click(hideBackgroundCheckbox)
736
727
  },
737
728
  (before, after) => before !== after
738
729
  )