@automattic/charts 4.12.0 → 4.13.0

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 (39) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/index.cjs +641 -636
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.css +40 -12
  5. package/dist/index.d.cts +26 -121
  6. package/dist/index.d.ts +26 -121
  7. package/dist/index.js +630 -613
  8. package/dist/index.js.map +1 -1
  9. package/package.json +1 -1
  10. package/src/charts/area-chart/area-chart.tsx +16 -6
  11. package/src/charts/area-chart/test/area-chart.test.tsx +42 -9
  12. package/src/charts/bar-chart/bar-chart.tsx +17 -12
  13. package/src/charts/bar-chart/private/index.ts +1 -1
  14. package/src/charts/bar-chart/private/slot-tooltip-members.tsx +4 -5
  15. package/src/charts/bar-chart/private/use-bar-stops.ts +1 -3
  16. package/src/charts/bar-chart/private/use-overlap-slots.ts +35 -16
  17. package/src/charts/bar-chart/test/bar-chart.test.tsx +115 -0
  18. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +24 -11
  19. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +235 -182
  20. package/src/charts/conversion-funnel-chart/index.ts +2 -0
  21. package/src/charts/conversion-funnel-chart/private/index.ts +1 -0
  22. package/src/charts/conversion-funnel-chart/private/use-funnel-selection.ts +9 -47
  23. package/src/charts/conversion-funnel-chart/private/use-roving-step-focus.ts +83 -0
  24. package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +312 -92
  25. package/src/charts/conversion-funnel-chart/test/use-funnel-selection.test.ts +23 -188
  26. package/src/charts/conversion-funnel-chart/types.ts +22 -0
  27. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +41 -1
  28. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +101 -30
  29. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +47 -0
  30. package/src/charts/line-chart/line-chart.tsx +10 -8
  31. package/src/charts/line-chart/test/line-chart.test.tsx +75 -0
  32. package/src/components/tooltip/private/at-nearest-date.ts +49 -0
  33. package/src/components/tooltip/private/keyboard-stops.ts +67 -0
  34. package/src/components/tooltip/private/xy-chart-tooltip-overlay.tsx +10 -2
  35. package/src/components/tooltip/test/keyboard-stops.test.ts +36 -0
  36. package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +135 -1
  37. package/src/components/tooltip/xy-chart-tooltip.tsx +8 -12
  38. package/src/index.ts +2 -0
  39. package/src/types.ts +4 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/charts",
3
- "version": "4.12.0",
3
+ "version": "4.13.0",
4
4
  "description": "Display charts within Automattic products.",
5
5
  "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/charts/#readme",
6
6
  "bugs": {
@@ -13,6 +13,7 @@ import {
13
13
  } from 'react';
14
14
  import { Legend, useChartLegendItems } from '../../components/legend';
15
15
  import { XYChartTooltip, useKeyboardNavigation } from '../../components/tooltip';
16
+ import { getKeyboardStops } from '../../components/tooltip/private/keyboard-stops';
16
17
  import {
17
18
  useXYChartTheme,
18
19
  useChartDataTransform,
@@ -144,14 +145,22 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
144
145
  [ seriesWithVisibility ]
145
146
  );
146
147
 
148
+ const visibleDataSeries = useMemo(
149
+ () => dataSorted.filter( series => isSeriesVisible( series.label ) ),
150
+ [ dataSorted, isSeriesVisible ]
151
+ );
152
+
147
153
  // A normalized stack (expand/wiggle/silhouette) turns whole-number data into
148
154
  // fractions, so the filter is skipped for every offset but 'none'.
149
155
  const hasWholeNumberValues = useMemo(
150
- () =>
151
- ( ! stacked || stackOffset === 'none' ) &&
152
- hasOnlyWholeNumbers( dataSorted.filter( series => isSeriesVisible( series.label ) ) ),
153
- [ dataSorted, isSeriesVisible, stacked, stackOffset ]
156
+ () => ( ! stacked || stackOffset === 'none' ) && hasOnlyWholeNumbers( visibleDataSeries ),
157
+ [ visibleDataSeries, stacked, stackOffset ]
158
+ );
159
+ const visibleSeriesKey = useMemo(
160
+ () => JSON.stringify( visibleDataSeries.map( series => series.label ) ),
161
+ [ visibleDataSeries ]
154
162
  );
163
+ const stops = useMemo( () => getKeyboardStops( visibleDataSeries ), [ visibleDataSeries ] );
155
164
 
156
165
  const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation( {
157
166
  selectedIndex,
@@ -159,7 +168,8 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
159
168
  isNavigating,
160
169
  setIsNavigating,
161
170
  chartRef,
162
- totalPoints: dataSorted[ 0 ]?.data.length || 0,
171
+ totalPoints: stops.count,
172
+ visibleSeriesKey,
163
173
  } );
164
174
 
165
175
  // Computed from the full data set (ignoring legend visibility) so the y-axis stays
@@ -526,7 +536,7 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
526
536
  keyboardFocusedClassName={
527
537
  styles[ 'area-chart__tooltip--keyboard-focused' ]
528
538
  }
529
- series={ dataSorted }
539
+ series={ visibleDataSeries }
530
540
  />
531
541
  <HoverGlyphs
532
542
  visibleSeries={ visibleSeries }
@@ -879,12 +879,6 @@ describe( 'AreaChart', () => {
879
879
  } );
880
880
 
881
881
  test( 'renders glyphs only for series with a datum at the hovered x (mismatched x-domains)', async () => {
882
- // Each series' data[0] is a different date. Keyboard nav fires
883
- // showTooltip for every series at its own data[selectedIndex];
884
- // the LAST one wins as the nearestDatum, so the resolved hover
885
- // date is Series B's. HoverGlyphs should:
886
- // - Skip Series A (no datum at Series B's date) — cumulative += 0
887
- // - Render a glyph for Series B
888
882
  renderWithProvider( {
889
883
  data: [
890
884
  {
@@ -901,9 +895,8 @@ describe( 'AreaChart', () => {
901
895
 
902
896
  const glyphs = screen.getAllByTestId( /^area-chart-hover-glyph-/ );
903
897
  expect( glyphs ).toHaveLength( 1 );
904
- // Only Series B (index 1) has a datum at the hovered date.
905
- expect( screen.getByTestId( 'area-chart-hover-glyph-1' ) ).toBeInTheDocument();
906
- expect( screen.queryByTestId( 'area-chart-hover-glyph-0' ) ).not.toBeInTheDocument();
898
+ expect( screen.getByTestId( 'area-chart-hover-glyph-0' ) ).toBeInTheDocument();
899
+ expect( screen.queryByTestId( 'area-chart-hover-glyph-1' ) ).not.toBeInTheDocument();
907
900
  } );
908
901
  } );
909
902
 
@@ -1055,4 +1048,44 @@ describe( 'AreaChart', () => {
1055
1048
  expect( screen.getAllByRole( 'button' )[ 1 ] ).toHaveAttribute( 'aria-pressed', 'true' );
1056
1049
  } );
1057
1050
  } );
1051
+
1052
+ it( 'gives no keyboard stop to a date only a hidden series has', async () => {
1053
+ const user = userEvent.setup();
1054
+
1055
+ render(
1056
+ <GlobalChartsProvider>
1057
+ <AreaChartUnresponsive
1058
+ width={ 500 }
1059
+ height={ 300 }
1060
+ stacked={ false }
1061
+ defaultHiddenSeries={ [ 'Series B' ] }
1062
+ chartId="test-hidden-only-date-area"
1063
+ data={ [
1064
+ {
1065
+ label: 'Series A',
1066
+ data: [
1067
+ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' },
1068
+ { date: new Date( '2024-01-02' ), value: 20, label: 'Jan 2' },
1069
+ ],
1070
+ },
1071
+ {
1072
+ label: 'Series B',
1073
+ data: [
1074
+ { date: new Date( '2024-01-01' ), value: 5, label: 'Jan 1' },
1075
+ { date: new Date( '2024-01-03' ), value: 15, label: 'Jan 3' },
1076
+ ],
1077
+ },
1078
+ ] }
1079
+ />
1080
+ </GlobalChartsProvider>
1081
+ );
1082
+
1083
+ screen.getByRole( 'application', { name: /area chart/i } ).focus();
1084
+ await user.keyboard( '{ArrowRight}{ArrowRight}{ArrowRight}' );
1085
+
1086
+ await expect( screen.findByRole( 'tooltip' ) ).resolves.toHaveAttribute(
1087
+ 'data-testid',
1088
+ 'chart-tooltip-1'
1089
+ );
1090
+ } );
1058
1091
  } );
@@ -47,6 +47,8 @@ import {
47
47
  useBarStops,
48
48
  useOverlapSlots,
49
49
  getSelectedSlot,
50
+ isSameCategory,
51
+ membersAt,
50
52
  } from './private';
51
53
  import { useBarColors } from './use-bar-colors';
52
54
  import { useBarLegendItems } from './use-bar-legend-items';
@@ -442,12 +444,9 @@ const BarChartInternal: FC< BarChartProps > = ( {
442
444
  // Find the comparison value paired with the hovered primary series (same group)
443
445
  // at the same category, so the tooltip can show both periods at once.
444
446
  const comparisonEntry = comparisonEntries.find( entry => entry.primaryKey === primaryKey );
445
- const comparisonDatum = comparisonEntry?.series.data.find( point => {
446
- const p = point as DataPointDate;
447
- return nearestDatum.label != null
448
- ? p.label === nearestDatum.label
449
- : !! nearestDatum.date && !! p.date && p.date.getTime() === nearestDatum.date.getTime();
450
- } ) as DataPointDate | undefined;
447
+ const comparisonDatum = comparisonEntry?.series.data.find( point =>
448
+ isSameCategory( point as DataPointDate, nearestDatum )
449
+ ) as DataPointDate | undefined;
451
450
 
452
451
  // With a paired comparison value, show the category as the header and one row
453
452
  // per period (primary + comparison).
@@ -536,6 +535,16 @@ const BarChartInternal: FC< BarChartProps > = ( {
536
535
  [ overlapSlots, selectedIndex ]
537
536
  );
538
537
 
538
+ const selectedGroup = useMemo( () => {
539
+ const stop = overlapSlots || selectedIndex === undefined ? undefined : barAt( selectedIndex );
540
+ if ( ! stop ) {
541
+ return undefined;
542
+ }
543
+ return membersAt( primaryEntries, stop.datum, stop.key );
544
+ }, [ overlapSlots, selectedIndex, barAt, primaryEntries ] );
545
+
546
+ const selectedMembers = selectedSlot?.members ?? selectedGroup;
547
+
539
548
  const selectedBar = useMemo( (): SelectedBar | undefined => {
540
549
  if ( overlapSlots ) {
541
550
  return selectedSlot;
@@ -783,11 +792,8 @@ const BarChartInternal: FC< BarChartProps > = ( {
783
792
  </WholeNumberTicks>
784
793
  ) }
785
794
 
786
- { withTooltips && selectedSlot && (
787
- <SlotTooltipMembers
788
- members={ selectedSlot.members }
789
- index={ selectedSlot.index }
790
- />
795
+ { withTooltips && selectedMembers && (
796
+ <SlotTooltipMembers members={ selectedMembers } />
791
797
  ) }
792
798
  { withTooltips && (
793
799
  <XYChartTooltip
@@ -806,7 +812,6 @@ const BarChartInternal: FC< BarChartProps > = ( {
806
812
  keyboardFocusedClassName={
807
813
  styles[ 'bar-chart__tooltip--keyboard-focused' ]
808
814
  }
809
- series={ stops.series }
810
815
  mode="individual"
811
816
  />
812
817
  ) }
@@ -5,7 +5,7 @@ export { default as SlottedBarSeries } from './slotted-bar-series';
5
5
  export { BarLayer } from './bar-layer';
6
6
  export { SelectedBarRing, type SelectedBar } from './selected-bar-ring';
7
7
  export { SlotTooltipMembers, type SlotMember } from './slot-tooltip-members';
8
- export { useOverlapSlots, getSelectedSlot, isSameCategory } from './use-overlap-slots';
8
+ export { useOverlapSlots, getSelectedSlot, isSameCategory, membersAt } from './use-overlap-slots';
9
9
  export { useBarStops } from './use-bar-stops';
10
10
  export { ComparisonBars } from './comparison-bars';
11
11
  export type { ComparisonSeriesEntry } from './comparison-bars';
@@ -3,7 +3,7 @@ import { useContext, useEffect } from 'react';
3
3
  import type { DataPointDate } from '../../../types';
4
4
 
5
5
  /** A bar at the keyboard-selected category. */
6
- export type SlotMember = { key: string; datum: DataPointDate };
6
+ export type SlotMember = { key: string; datum: DataPointDate; index: number };
7
7
 
8
8
  /**
9
9
  * Fills the tooltip with every bar of the keyboard-selected category, and only those.
@@ -12,18 +12,17 @@ export type SlotMember = { key: string; datum: DataPointDate };
12
12
  *
13
13
  * @param props - Component props.
14
14
  * @param props.members - The category's bars, the keyboard stop last.
15
- * @param props.index - The selected data point.
16
15
  * @return No visual content.
17
16
  */
18
- export function SlotTooltipMembers( { members, index }: { members: SlotMember[]; index: number } ) {
17
+ export function SlotTooltipMembers( { members }: { members: SlotMember[] } ) {
19
18
  const { showTooltip, updateTooltip } = useContext( TooltipContext ) ?? {};
20
19
 
21
20
  useEffect( () => {
22
21
  updateTooltip?.( state => ( { ...state, tooltipData: undefined } ) );
23
- members.forEach( ( { key, datum } ) => showTooltip?.( { key, datum, index } ) );
22
+ members.forEach( member => showTooltip?.( member ) );
24
23
  // `showTooltip` and `updateTooltip` are stable; the context object is not.
25
24
  // eslint-disable-next-line react-hooks/exhaustive-deps
26
- }, [ members, index ] );
25
+ }, [ members ] );
27
26
 
28
27
  return null;
29
28
  }
@@ -10,8 +10,6 @@ export type StopBar = { datum: DataPointDate; index: number; key: string };
10
10
 
11
11
  export type BarStops = {
12
12
  count: number;
13
- /** The series `XYChartTooltip` reads a stop's datum from. Overlap has none: `SlotTooltipMembers` fills its tooltip. */
14
- series: SeriesData[];
15
13
  /** Changes when the stops do, so keyboard navigation starts over. */
16
14
  key: string;
17
15
  barAt: ( stop: number ) => StopBar | undefined;
@@ -85,5 +83,5 @@ export function useBarStops(
85
83
  [ overlapSlots, series ]
86
84
  );
87
85
 
88
- return { count, series, key, barAt, stopOf };
86
+ return { count, key, barAt, stopOf };
89
87
  }
@@ -27,7 +27,7 @@ export type StopPosition = {
27
27
  dataPointIndex: number;
28
28
  };
29
29
 
30
- export type SelectedSlot = SelectedBar & { index: number; members: SlotMember[] };
30
+ export type SelectedSlot = SelectedBar & { members: SlotMember[] };
31
31
 
32
32
  /**
33
33
  * Whether two data points sit at the same category: the same label, or the same date.
@@ -37,7 +37,8 @@ export type SelectedSlot = SelectedBar & { index: number; members: SlotMember[]
37
37
  * @return Whether `point` is at `category`.
38
38
  */
39
39
  export function isSameCategory( point: DataPointDate, category: DataPointDate ): boolean {
40
- return category.label != null
40
+ // An empty label falls back to the date, as the band axis's accessor does.
41
+ return category.label
41
42
  ? point.label === category.label
42
43
  : !! category.date && !! point.date && point.date.getTime() === category.date.getTime();
43
44
  }
@@ -101,7 +102,7 @@ export function useOverlapSlots(
101
102
  * @return The category key.
102
103
  */
103
104
  function categoryKey( point: DataPointDate ): string {
104
- return point.label != null ? `label:${ point.label }` : `date:${ point.date?.getTime() }`;
105
+ return point.label ? `label:${ point.label }` : `date:${ point.date?.getTime() }`;
105
106
  }
106
107
 
107
108
  /**
@@ -152,6 +153,34 @@ function getStopPositions(
152
153
  );
153
154
  }
154
155
 
156
+ /**
157
+ * Every bar at a category, each with its own data point index, the keyboard stop last.
158
+ *
159
+ * @param entries - The visible series.
160
+ * @param category - The data point naming the category.
161
+ * @param stopKey - The series the keyboard stop is on.
162
+ * @return The category's bars; the last one shown becomes the tooltip's nearest datum.
163
+ */
164
+ export function membersAt(
165
+ entries: SeriesEntry[],
166
+ category: DataPointDate,
167
+ stopKey: string
168
+ ): SlotMember[] {
169
+ const members = entries.flatMap( ( { series } ) => {
170
+ const index = ( series.data as DataPointDate[] ).findIndex( point =>
171
+ isSameCategory( point, category )
172
+ );
173
+ return index === -1
174
+ ? []
175
+ : [ { key: series.label, datum: series.data[ index ] as DataPointDate, index } ];
176
+ } );
177
+
178
+ return [
179
+ ...members.filter( ( { key } ) => key !== stopKey ),
180
+ ...members.filter( ( { key } ) => key === stopKey ),
181
+ ];
182
+ }
183
+
155
184
  /**
156
185
  * The slot a keyboard index selects, and every bar at its category.
157
186
  *
@@ -169,13 +198,8 @@ export function getSelectedSlot(
169
198
  return undefined;
170
199
  }
171
200
 
172
- const { slotKey, key: stopKey, datum: category, dataPointIndex } = position;
173
- const members = slots.entries.flatMap( ( { series } ) => {
174
- const datum = ( series.data as DataPointDate[] ).find( point =>
175
- isSameCategory( point, category )
176
- );
177
- return datum ? [ { key: series.label, datum } ] : [];
178
- } );
201
+ const { slotKey, key: stopKey, datum: category } = position;
202
+ const members = membersAt( slots.entries, category, stopKey );
179
203
  const slotBars = members.filter(
180
204
  ( { key, datum } ) => isBarRendered( datum ) && slots.slotOf( key ) === slotKey
181
205
  );
@@ -184,11 +208,6 @@ export function getSelectedSlot(
184
208
  data: slotBars.map( ( { datum } ) => datum ),
185
209
  slotKey,
186
210
  thickness: Math.max( ...slotBars.map( ( { key } ) => slots.thicknessOf( key ) ) ),
187
- index: dataPointIndex,
188
- // The stop goes last: the last bar shown becomes the tooltip's nearest datum.
189
- members: [
190
- ...members.filter( ( { key } ) => key !== stopKey ),
191
- ...members.filter( ( { key } ) => key === stopKey ),
192
- ],
211
+ members,
193
212
  };
194
213
  }
@@ -17,6 +17,7 @@ import { useBarChartOptions } from '../private';
17
17
  import { useBarColors } from '../use-bar-colors';
18
18
  import type { GlobalChartsContextValue } from '../../../providers/chart-context/types';
19
19
  import type { DataPointDate, SeriesData } from '../../../types';
20
+ import type { RenderTooltipParams } from '../../../visx/types';
20
21
 
21
22
  // Mock useElementSize to return non-zero dimensions in jsdom so charts render
22
23
  const mockRefCallback = jest.fn();
@@ -2025,6 +2026,39 @@ describe( 'BarChart', () => {
2025
2026
  expect( tooltip ).toHaveTextContent( 'Previous period: 15' );
2026
2027
  } );
2027
2028
 
2029
+ test( 'pairs the comparison value by date when the bars have empty labels', async () => {
2030
+ const user = userEvent.setup();
2031
+ renderWithTheme( {
2032
+ withTooltips: true,
2033
+ data: [
2034
+ {
2035
+ label: 'This period',
2036
+ group: 'views',
2037
+ data: [
2038
+ { date: new Date( '2024-01-01' ), value: 10, label: '' },
2039
+ { date: new Date( '2024-01-02' ), value: 20, label: '' },
2040
+ ],
2041
+ },
2042
+ {
2043
+ label: 'Previous period',
2044
+ group: 'views',
2045
+ options: { type: 'comparison' as const },
2046
+ data: [
2047
+ { date: new Date( '2024-01-01' ), value: 15, label: '' },
2048
+ { date: new Date( '2024-01-02' ), value: 25, label: '' },
2049
+ ],
2050
+ },
2051
+ ],
2052
+ } );
2053
+
2054
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
2055
+ await user.keyboard( '{ArrowRight}{ArrowRight}' );
2056
+
2057
+ const tooltip = screen.getByTestId( 'chart-tooltip-1' );
2058
+ expect( tooltip ).toHaveTextContent( 'This period: 20' );
2059
+ expect( tooltip ).toHaveTextContent( 'Previous period: 25' );
2060
+ } );
2061
+
2028
2062
  test( 'keyboard navigation past the first slot stays on the primary bar, not the comparison series', async () => {
2029
2063
  const user = userEvent.setup();
2030
2064
  renderWithTheme( {
@@ -3554,6 +3588,87 @@ describe( 'BarChart', () => {
3554
3588
  expect( screen.queryByRole( 'tooltip' ) ).not.toBeInTheDocument();
3555
3589
  } );
3556
3590
 
3591
+ const labelledViews = [
3592
+ { label: 'Apr', value: 29 },
3593
+ { label: 'May', value: 19 },
3594
+ ];
3595
+ const april = new Date( '2025-04-01T00:00:00Z' );
3596
+ const may = new Date( '2025-05-01T00:00:00Z' );
3597
+
3598
+ it.each( [
3599
+ {
3600
+ name: 'aligned series',
3601
+ views: labelledViews,
3602
+ visitors: [
3603
+ { label: 'Apr', value: 112 },
3604
+ { label: 'May', value: 80 },
3605
+ ],
3606
+ presses: 3,
3607
+ rows: [ 'Views=19@1', 'Visitors=80@1' ],
3608
+ nearest: 'Views',
3609
+ },
3610
+ {
3611
+ name: 'ragged series',
3612
+ views: labelledViews,
3613
+ visitors: [ { label: 'May', value: 80 } ],
3614
+ presses: 2,
3615
+ rows: [ 'Views=19@1', 'Visitors=80@0' ],
3616
+ nearest: 'Visitors',
3617
+ },
3618
+ {
3619
+ name: 'dated points with empty labels',
3620
+ views: [
3621
+ { date: april, label: '', value: 29 },
3622
+ { date: may, label: '', value: 19 },
3623
+ ],
3624
+ visitors: [
3625
+ { date: april, label: '', value: 112 },
3626
+ { date: may, label: '', value: 80 },
3627
+ ],
3628
+ presses: 3,
3629
+ rows: [ 'Views=19@1', 'Visitors=80@1' ],
3630
+ nearest: 'Views',
3631
+ },
3632
+ ] )(
3633
+ 'gives the keyboard tooltip only the bars at the selected category: $name',
3634
+ async ( { views, visitors, presses, rows, nearest } ) => {
3635
+ const user = userEvent.setup();
3636
+ const renderTooltip = ( { tooltipData }: RenderTooltipParams< DataPointDate > ) => (
3637
+ <div>
3638
+ <span data-testid="nearest">{ tooltipData?.nearestDatum?.key }</span>
3639
+ { Object.entries( tooltipData?.datumByKey ?? {} ).map(
3640
+ ( [ key, { datum, index } ] ) => (
3641
+ <span
3642
+ key={ key }
3643
+ data-testid="row"
3644
+ >{ `${ key }=${ datum.value }@${ index }` }</span>
3645
+ )
3646
+ ) }
3647
+ </div>
3648
+ );
3649
+
3650
+ renderWithTheme( {
3651
+ withTooltips: true,
3652
+ renderTooltip,
3653
+ data: [
3654
+ { label: 'Views', data: views, options: {} },
3655
+ { label: 'Visitors', data: visitors, options: {} },
3656
+ ],
3657
+ } );
3658
+
3659
+ screen.getByRole( 'application', { name: /bar chart/i } ).focus();
3660
+ for ( let i = 0; i < presses; i++ ) {
3661
+ await user.keyboard( '{ArrowRight}' );
3662
+ }
3663
+
3664
+ expect( screen.getAllByTestId( 'row' ).map( row => row.textContent ) ).toEqual(
3665
+ expect.arrayContaining( rows )
3666
+ );
3667
+ expect( screen.getAllByTestId( 'row' ) ).toHaveLength( rows.length );
3668
+ expect( screen.getByTestId( 'nearest' ) ).toHaveTextContent( nearest );
3669
+ }
3670
+ );
3671
+
3557
3672
  it( 'stops at the last visible slot on a standard chart', async () => {
3558
3673
  const user = userEvent.setup();
3559
3674
 
@@ -52,13 +52,28 @@
52
52
  height: 100%;
53
53
 
54
54
  &--animated {
55
- transition:
56
- opacity var(--wpds-motion-duration-lg)
57
- var(--wpds-motion-easing-subtle);
55
+
56
+ .step-text,
57
+ .funnel-bar,
58
+ .bar-container {
59
+ transition:
60
+ opacity var(--wpds-motion-duration-lg) var(--wpds-motion-easing-subtle),
61
+ background-color var(--wpds-motion-duration-lg) var(--wpds-motion-easing-subtle);
62
+ }
58
63
  }
59
64
 
65
+ // Dim the contents, not the step: opacity on an ancestor would also fade
66
+ // the bar's focus ring.
60
67
  &--blurred {
61
- opacity: 0.3;
68
+
69
+ .step-text,
70
+ .funnel-bar {
71
+ opacity: 0.3;
72
+ }
73
+
74
+ .bar-container {
75
+ background-color: color-mix(in sRGB, var(--a8c-charts-color-surface-secondary) 30%, transparent);
76
+ }
62
77
  }
63
78
  }
64
79
 
@@ -119,20 +134,18 @@
119
134
  }
120
135
  }
121
136
 
122
- .tooltip-title {
137
+ .tooltip-title,
138
+ .tooltip-label,
139
+ .tooltip-value {
123
140
  color: var(--a8c-charts-color-label);
124
141
  font-size: var(--wpds-typography-font-size-sm);
125
142
  font-style: normal;
126
143
  font-weight: var(--wpds-typography-font-weight-default);
127
- line-height: var(--wpds-typography-line-height-xs);
144
+ line-height: var(--wpds-typography-line-height-sm);
128
145
  }
129
146
 
130
- .tooltip-content {
131
- color: var(--a8c-charts-color-label);
132
- font-size: var(--wpds-typography-font-size-md);
133
- font-style: normal;
147
+ .tooltip-value {
134
148
  font-weight: var(--wpds-typography-font-weight-emphasis);
135
- line-height: var(--wpds-typography-line-height-sm);
136
149
  }
137
150
 
138
151
  .empty-state {