@automattic/charts 4.8.0 → 4.9.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 (56) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.cjs +624 -228
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.css +58 -8
  5. package/dist/index.d.cts +42 -8
  6. package/dist/index.d.ts +42 -8
  7. package/dist/index.js +628 -232
  8. package/dist/index.js.map +1 -1
  9. package/package.json +1 -1
  10. package/src/charts/area-chart/area-chart.tsx +5 -2
  11. package/src/charts/area-chart/test/area-chart.test.tsx +23 -14
  12. package/src/charts/area-chart/test/tooltip-resolution.test.tsx +2 -2
  13. package/src/charts/area-chart/types.ts +2 -0
  14. package/src/charts/bar-chart/bar-chart.tsx +5 -2
  15. package/src/charts/bar-chart/test/bar-chart.test.tsx +82 -63
  16. package/src/charts/bar-chart/types.ts +2 -0
  17. package/src/charts/bar-list-chart/test/bar-list-chart.test.tsx +3 -1
  18. package/src/charts/heatmap-chart/heatmap-chart.module.scss +10 -4
  19. package/src/charts/heatmap-chart/heatmap-chart.tsx +144 -37
  20. package/src/charts/heatmap-chart/private/column-groups.ts +79 -8
  21. package/src/charts/heatmap-chart/private/fit-compact-cells.ts +72 -0
  22. package/src/charts/heatmap-chart/private/heatmap-legend.tsx +14 -2
  23. package/src/charts/heatmap-chart/private/index.ts +4 -2
  24. package/src/charts/heatmap-chart/test/column-groups.test.ts +41 -4
  25. package/src/charts/heatmap-chart/test/fit-compact-cells.test.ts +59 -0
  26. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +132 -1
  27. package/src/charts/heatmap-chart/types.ts +2 -0
  28. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +46 -4
  29. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +37 -1
  30. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +53 -0
  31. package/src/charts/leaderboard-chart/types.ts +6 -1
  32. package/src/charts/line-chart/line-chart.tsx +5 -2
  33. package/src/charts/line-chart/private/line-chart-annotation-label-popover.tsx +19 -9
  34. package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +1 -1
  35. package/src/charts/line-chart/test/line-chart.test.tsx +45 -35
  36. package/src/charts/line-chart/test/tooltip-resolution.test.tsx +2 -2
  37. package/src/charts/line-chart/types.ts +5 -3
  38. package/src/charts/pie-chart/pie-chart.tsx +149 -139
  39. package/src/charts/pie-chart/test/pie-chart.test.tsx +171 -0
  40. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +122 -120
  41. package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +54 -0
  42. package/src/charts/private/chart-composition/use-chart-children.ts +11 -6
  43. package/src/charts/private/pie-keyboard-navigation/index.ts +4 -0
  44. package/src/charts/private/pie-keyboard-navigation/order-arcs-for-navigation.ts +10 -0
  45. package/src/charts/private/pie-keyboard-navigation/pie-keyboard-navigation.module.scss +16 -0
  46. package/src/charts/private/pie-keyboard-navigation/pie-selection-announcement.tsx +14 -0
  47. package/src/charts/private/pie-keyboard-navigation/pie-selection-output.tsx +82 -0
  48. package/src/charts/private/pie-keyboard-navigation/pie-tooltip.tsx +44 -0
  49. package/src/charts/private/pie-keyboard-navigation/selected-segment-ring.tsx +47 -0
  50. package/src/charts/private/pie-keyboard-navigation/test/order-arcs-for-navigation.test.ts +34 -0
  51. package/src/charts/private/pie-keyboard-navigation/test/selected-segment-ring.test.ts +14 -0
  52. package/src/charts/private/pie-keyboard-navigation/use-pie-keyboard-navigation.ts +139 -0
  53. package/src/charts/private/x-zoom/test/x-zoom.test.tsx +3 -2
  54. package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +20 -19
  55. package/src/components/tooltip/xy-chart-tooltip.tsx +3 -3
  56. package/src/types.ts +18 -2
@@ -1,4 +1,4 @@
1
- import { render, screen, within } from '@testing-library/react';
1
+ import { act, render, screen, within } from '@testing-library/react';
2
2
  import userEvent from '@testing-library/user-event';
3
3
  import { GlobalChartsProvider } from '../../../providers';
4
4
  import { buildMonthCalendarHeatmapData } from '../build-month-calendar-data';
@@ -172,6 +172,29 @@ describe( 'HeatmapChart', () => {
172
172
  ).toHaveLength( 4 );
173
173
  } );
174
174
 
175
+ test.each( [
176
+ [ 'with zeros painted empty', data, 1 ],
177
+ [ 'with negative values', [ { label: 'W1', data: [ { value: -2 }, { value: 0 } ] } ], 0 ],
178
+ ] )( 'starts the legend scale with an empty-cell swatch %s', ( _case, rows, count ) => {
179
+ render(
180
+ <GlobalChartsProvider>
181
+ <HeatmapChart width={ 500 } height={ 300 } data={ rows }>
182
+ <HeatmapChart.Legend variant="bar" />
183
+ </HeatmapChart>
184
+ </GlobalChartsProvider>
185
+ );
186
+ const empty = screen.queryAllByTestId( 'heatmap-legend-empty-swatch' );
187
+ const scale = screen.getByTestId( 'heatmap-legend-scale' );
188
+
189
+ expect( empty ).toHaveLength( count );
190
+ expect( within( scale ).getAllByTestId( 'heatmap-legend-swatch' ) ).toHaveLength( 5 );
191
+ expect(
192
+ within( scale )
193
+ .getAllByTestId( /^heatmap-legend-(empty-)?swatch$/ )
194
+ .slice( 0, count )
195
+ ).toEqual( empty );
196
+ } );
197
+
175
198
  test( 'paints the legend on the same scale as the cells', () => {
176
199
  render(
177
200
  <GlobalChartsProvider>
@@ -943,6 +966,114 @@ describe( 'HeatmapChart column groups', () => {
943
966
  } );
944
967
  } );
945
968
 
969
+ describe( 'HeatmapChart fitCells', () => {
970
+ const threeMonths = Array.from( { length: 6 }, ( _, index ) => ( {
971
+ data: [ { value: index }, { value: index + 1 } ],
972
+ } ) );
973
+ const columnGroups = [
974
+ { label: 'Jan', span: 2 },
975
+ { label: 'Feb', span: 2 },
976
+ { label: 'Mar', span: 2 },
977
+ ];
978
+
979
+ let widthSpy: jest.SpyInstance;
980
+ let heightSpy: jest.SpyInstance;
981
+
982
+ beforeEach( () => {
983
+ widthSpy = jest.spyOn( Element.prototype, 'clientWidth', 'get' ).mockReturnValue( 400 );
984
+ heightSpy = jest.spyOn( Element.prototype, 'clientHeight', 'get' ).mockReturnValue( 400 );
985
+ } );
986
+
987
+ afterEach( () => {
988
+ widthSpy.mockRestore();
989
+ heightSpy.mockRestore();
990
+ } );
991
+
992
+ test( 'wraps the groups that fit better on a second band below the first, cells grown to match', () => {
993
+ renderChart( { data: threeMonths, columnGroups, compact: true, fitCells: true } );
994
+ const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
995
+ expect( grid.style.getPropertyValue( '--a8c-charts-dimension-heatmap-cell-size' ) ).toBe(
996
+ '91px'
997
+ );
998
+ const [ , feb, mar ] = screen.getAllByTestId( 'heatmap-group-label' );
999
+ expect( feb ).toHaveStyle( { gridColumn: '5 / span 2', gridRow: '3' } );
1000
+ // Band one takes rows 1-2, its labels 3 and the gap 4, so band two's labels sit on 7.
1001
+ expect( mar ).toHaveStyle( { gridColumn: '2 / span 2', gridRow: '7' } );
1002
+ const marchCell = screen
1003
+ .getAllByTestId( 'heatmap-cell' )
1004
+ .find( element => element.dataset.column === '4' && element.dataset.row === '0' );
1005
+ expect( marchCell ).toHaveStyle( { gridColumn: '2', gridRow: '5' } );
1006
+ } );
1007
+
1008
+ test( 'keeps the theme size, with a warning, when a column is a summary', () => {
1009
+ renderChart( {
1010
+ data: [
1011
+ ...threeMonths,
1012
+ { label: 'Total', summary: true, data: [ { value: 1 }, { value: 2 } ] },
1013
+ ],
1014
+ columnGroups,
1015
+ compact: true,
1016
+ fitCells: true,
1017
+ } );
1018
+ const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
1019
+ expect( grid.style.getPropertyValue( '--a8c-charts-dimension-heatmap-cell-size' ) ).toBe(
1020
+ '11px'
1021
+ );
1022
+ expect( console ).toHaveWarned();
1023
+ } );
1024
+
1025
+ test( 'leaves the grid the box less the content beside it', () => {
1026
+ const offsetHeight = jest
1027
+ .spyOn( HTMLElement.prototype, 'offsetHeight', 'get' )
1028
+ .mockImplementation( function ( this: HTMLElement ) {
1029
+ return this.dataset.testid === 'beside-grid' ? 100 : 0;
1030
+ } );
1031
+ try {
1032
+ renderChart( {
1033
+ data: threeMonths,
1034
+ columnGroups,
1035
+ compact: true,
1036
+ fitCells: true,
1037
+ children: <div data-testid="beside-grid" />,
1038
+ } );
1039
+ const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
1040
+ expect( grid.style.getPropertyValue( '--a8c-charts-dimension-heatmap-cell-size' ) ).toBe(
1041
+ '66px'
1042
+ );
1043
+ } finally {
1044
+ offsetHeight.mockRestore();
1045
+ }
1046
+ } );
1047
+
1048
+ test( 'refits within the resize delivery, before later observers read the grid', () => {
1049
+ const observers: ResizeObserverCallback[] = [];
1050
+ const original = window.ResizeObserver;
1051
+ window.ResizeObserver = class {
1052
+ constructor( private callback: ResizeObserverCallback ) {}
1053
+ observe() {
1054
+ observers.push( this.callback );
1055
+ }
1056
+ unobserve() {}
1057
+ disconnect() {}
1058
+ } as unknown as typeof ResizeObserver;
1059
+ try {
1060
+ renderChart( { data: threeMonths, columnGroups, compact: true, fitCells: true } );
1061
+ const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
1062
+ heightSpy.mockReturnValue( 100 );
1063
+ let cellSize = '';
1064
+ act( () => {
1065
+ observers.forEach( callback => callback( [], {} as ResizeObserver ) );
1066
+ cellSize = grid.style.getPropertyValue( '--a8c-charts-dimension-heatmap-cell-size' );
1067
+ } );
1068
+ expect( cellSize ).toBe( '48px' );
1069
+ const [ , , mar ] = screen.getAllByTestId( 'heatmap-group-label' );
1070
+ expect( mar ).toHaveStyle( { gridRow: '3' } );
1071
+ } finally {
1072
+ window.ResizeObserver = original;
1073
+ }
1074
+ } );
1075
+ } );
1076
+
946
1077
  describe( 'HeatmapChart calendar navigation', () => {
947
1078
  const calendar = ( range: { start: string; end: string }, days: string[] ) =>
948
1079
  buildMonthCalendarHeatmapData(
@@ -78,6 +78,8 @@ export interface HeatmapChartProps extends Omit<
78
78
  ariaLabel?: string;
79
79
  /** Compact mode: hide in-cell values, tighten gap, thin axis labels. Default false. */
80
80
  compact?: boolean;
81
+ /** Compact mode only: grow the cells to fill the chart's box, wrapping the column groups onto more rows when that makes them larger. Default false. */
82
+ fitCells?: boolean;
81
83
  /** Render the numeric value inside each cell. Default `! compact`. */
82
84
  showValues?: boolean;
83
85
  /**
@@ -193,8 +193,10 @@ $focus-ring-width: var(--wpds-border-width-focus);
193
193
  var(--wpds-motion-easing-subtle);
194
194
  }
195
195
 
196
- &:hover,
197
- &:focus-visible {
196
+ // A layered row's own link or button does not advertise the drill-down.
197
+ &:hover:not(:has(:is(a, button):not(.rowButton):hover)),
198
+ &:focus-visible,
199
+ &:has(> .rowButton:focus-visible) {
198
200
 
199
201
  // Fixed reserve for the chevron — the value slides by it in full, and the
200
202
  // bar width inset (see leaderboard-chart.tsx) scales it by the bar's share.
@@ -214,8 +216,9 @@ $focus-ring-width: var(--wpds-border-width-focus);
214
216
  }
215
217
 
216
218
  // In RTL the value slides toward the inline start the other way (rightward).
217
- [dir="rtl"] &:hover .valueContainer,
218
- [dir="rtl"] &:focus-visible .valueContainer {
219
+ [dir="rtl"] &:hover:not(:has(:is(a, button):not(.rowButton):hover)) .valueContainer,
220
+ [dir="rtl"] &:focus-visible .valueContainer,
221
+ [dir="rtl"] &:has(> .rowButton:focus-visible) .valueContainer {
219
222
  transform: translateX(var(--a8c-charts-dimension-leaderboard-bar-hover-inset));
220
223
  }
221
224
 
@@ -231,6 +234,45 @@ $focus-ring-width: var(--wpds-border-width-focus);
231
234
  }
232
235
  }
233
236
 
237
+ // A row whose label holds a link: the button follows the cells for tab order
238
+ // but is layered beneath them, and the cells pass clicks through except on the
239
+ // label's own links and buttons. The label's isolate traps their z-index, so
240
+ // layering the cells is what keeps them above the button.
241
+ .layeredRow {
242
+ isolation: isolate;
243
+
244
+ > :not(.rowButton, .chevron) {
245
+ position: relative;
246
+ z-index: 1;
247
+ pointer-events: none;
248
+ }
249
+
250
+ :is(a, button):not(.rowButton) {
251
+ position: relative;
252
+ z-index: 1;
253
+ pointer-events: auto;
254
+ }
255
+ }
256
+
257
+ .rowButton {
258
+ position: absolute;
259
+ inset: 0;
260
+ appearance: none;
261
+ padding: 0;
262
+ border: 0;
263
+ background: none;
264
+ cursor: pointer;
265
+ z-index: 0;
266
+
267
+ &:focus-visible {
268
+ border-radius: var(--wpds-border-radius-sm);
269
+ outline:
270
+ $focus-ring-width solid
271
+ var(--wpds-color-stroke-focus);
272
+ outline-offset: calc(-1 * #{$focus-ring-width});
273
+ }
274
+ }
275
+
234
276
  .chevron {
235
277
  position: absolute;
236
278
  inset-inline-end: var(--wpds-dimension-padding-xs);
@@ -4,7 +4,7 @@ import { __ } from '@wordpress/i18n';
4
4
  import { Icon, chevronRight } from '@wordpress/icons';
5
5
  import { Stack, Text } from '@wordpress/ui';
6
6
  import clsx from 'clsx';
7
- import { useContext, useMemo, type FC } from 'react';
7
+ import { useContext, useMemo, type FC, type MouseEvent } from 'react';
8
8
  import { Legend } from '../../components/legend';
9
9
  import { usePrefersReducedMotion } from '../../hooks';
10
10
  import {
@@ -89,6 +89,20 @@ const hasDeltaValue = (
89
89
  delta: number;
90
90
  } => hasPreviousValue( entry ) && entry.delta != null;
91
91
 
92
+ /**
93
+ * A layered row's cell that takes pointer events of its own (a value tooltip)
94
+ * sits above the row button; pass its clicks on so they still drill down.
95
+ *
96
+ * @param event - The click on the row.
97
+ */
98
+ function forwardCellClickToRowButton( event: MouseEvent< HTMLDivElement > ) {
99
+ if ( ( event.target as Element ).closest( 'a, button' ) ) {
100
+ return;
101
+ }
102
+
103
+ event.currentTarget.querySelector< HTMLButtonElement >( ':scope > button' )?.click();
104
+ }
105
+
92
106
  const BarLabel = ( { label }: { label: LeaderboardEntry[ 'label' ] } ) => (
93
107
  <>{ typeof label === 'string' ? <Text className={ styles.label }>{ label }</Text> : label }</>
94
108
  );
@@ -449,6 +463,28 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
449
463
  </>
450
464
  );
451
465
 
466
+ if ( entry.onClick && entry.hasInteractiveLabel ) {
467
+ return (
468
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- Mouse-only fallback; the row button carries keyboard access.
469
+ <div
470
+ key={ entry.id }
471
+ data-row-index={ rowIndex }
472
+ style={ rowStyle }
473
+ className={ clsx( styles.row, styles.interactiveRow, styles.layeredRow ) }
474
+ onClick={ forwardCellClickToRowButton }
475
+ >
476
+ { rowCells }
477
+ <button
478
+ type="button"
479
+ className={ styles.rowButton }
480
+ onClick={ entry.onClick }
481
+ aria-label={ entry.ariaLabel }
482
+ />
483
+ <Icon className={ styles.chevron } icon={ chevronRight } size={ 24 } />
484
+ </div>
485
+ );
486
+ }
487
+
452
488
  if ( entry.onClick ) {
453
489
  return (
454
490
  <button
@@ -547,6 +547,59 @@ describe( 'LeaderboardChart', () => {
547
547
  expect( screen.getByRole( 'button' ).tagName ).toBe( 'BUTTON' );
548
548
  } );
549
549
 
550
+ it( 'keeps a label link beside the row button, not inside it, when the label is interactive', async () => {
551
+ const user = userEvent.setup();
552
+ const onClick = jest.fn();
553
+ render(
554
+ <LeaderboardChart
555
+ data={ [
556
+ {
557
+ ...mockData[ 0 ],
558
+ label: <a href="/author/1">Author</a>,
559
+ onClick,
560
+ ariaLabel: 'View posts by Author',
561
+ hasInteractiveLabel: true,
562
+ },
563
+ ] }
564
+ />
565
+ );
566
+
567
+ const button = screen.getByRole( 'button', { name: 'View posts by Author' } );
568
+ const link = screen.getByRole( 'link', { name: 'Author' } );
569
+ link.addEventListener( 'click', event => event.preventDefault() );
570
+ expect( button ).not.toContainElement( link );
571
+
572
+ await user.click( button );
573
+ expect( onClick ).toHaveBeenCalledTimes( 1 );
574
+ await user.click( link );
575
+ expect( onClick ).toHaveBeenCalledTimes( 1 );
576
+ // A value tooltip takes pointer events above the button; its click still drills down.
577
+ await user.click( screen.getByText( '12.5K' ) );
578
+ expect( onClick ).toHaveBeenCalledTimes( 2 );
579
+ } );
580
+
581
+ it( 'tabs to the label link before the row button when the label is interactive', async () => {
582
+ const user = userEvent.setup();
583
+ render(
584
+ <LeaderboardChart
585
+ data={ [
586
+ {
587
+ ...mockData[ 0 ],
588
+ label: <a href="/author/1">Author</a>,
589
+ onClick: jest.fn(),
590
+ ariaLabel: 'View posts by Author',
591
+ hasInteractiveLabel: true,
592
+ },
593
+ ] }
594
+ />
595
+ );
596
+
597
+ await user.tab();
598
+ expect( screen.getByRole( 'link', { name: 'Author' } ) ).toHaveFocus();
599
+ await user.tab();
600
+ expect( screen.getByRole( 'button', { name: 'View posts by Author' } ) ).toHaveFocus();
601
+ } );
602
+
550
603
  it( 'calls onClick when the row is clicked', async () => {
551
604
  const user = userEvent.setup();
552
605
  const onClick = jest.fn();
@@ -4,7 +4,6 @@ import { BaseChartProps, LeaderboardEntry } from '../../types';
4
4
  export interface LeaderboardChartProps extends Pick<
5
5
  BaseChartProps< LeaderboardEntry >,
6
6
  | 'className'
7
- | 'data'
8
7
  | 'showLegend'
9
8
  | 'legend'
10
9
  | 'chartId'
@@ -14,6 +13,12 @@ export interface LeaderboardChartProps extends Pick<
14
13
  | 'gap'
15
14
  | 'animation'
16
15
  > {
16
+ /**
17
+ * Entries to display. Declared here rather than picked, since the base type
18
+ * would split the entry union into one array type per member.
19
+ */
20
+ data: LeaderboardEntry[];
21
+
17
22
  /**
18
23
  * Whether to show comparison data
19
24
  */
@@ -232,6 +232,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
232
232
  (
233
233
  {
234
234
  data,
235
+ ariaLabel,
235
236
  chartId: providedChartId,
236
237
  width,
237
238
  height,
@@ -624,10 +625,11 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
624
625
  const chartHeight = contentHeight > 0 ? contentHeight : height;
625
626
 
626
627
  return (
628
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the application role hands arrow keys to the chart's point navigation.
627
629
  <div
628
630
  ref={ chartRef }
629
- role="grid"
630
- aria-label={ __( 'Line chart', 'jetpack-charts' ) }
631
+ role="application"
632
+ aria-label={ ariaLabel ?? __( 'Line chart', 'jetpack-charts' ) }
631
633
  tabIndex={ 0 }
632
634
  onKeyDown={ onChartKeyDown }
633
635
  onFocus={ onChartFocus }
@@ -637,6 +639,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
637
639
  <div className={ plotStyles[ 'xy-plot' ] }>
638
640
  { zoomable && zoom.domain && <ZoomResetButton onClick={ zoom.reset } /> }
639
641
  <XYChart
642
+ accessibilityLabel=""
640
643
  theme={ theme }
641
644
  width={ width }
642
645
  height={ chartHeight }
@@ -1,14 +1,17 @@
1
1
  import { __ } from '@wordpress/i18n';
2
2
  import { Stack } from '@wordpress/ui';
3
3
  import clsx from 'clsx';
4
- import { useEffect, useId, useRef, useState } from 'react';
4
+ import { useEffect, useId, useRef, useState, version as reactVersion } from 'react';
5
5
  import { isSafari } from '../../../utils';
6
6
  import styles from '../line-chart.module.scss';
7
7
  import type { ButtonWithPopover, PopoverElement, ToggleEvent } from '../../../types';
8
- import type { FC } from 'react';
8
+ import type { FC, ReactNode } from 'react';
9
9
 
10
10
  export const POPOVER_BUTTON_SIZE = 44;
11
11
 
12
+ // @todo Remove this (and uses of it below) when we drop support for WordPress versions shipping React 18.
13
+ const isReact18 = reactVersion.startsWith( '18.' );
14
+
12
15
  const CloseIcon = () => (
13
16
  <svg
14
17
  width="16"
@@ -29,8 +32,8 @@ const CloseIcon = () => (
29
32
  interface LineChartAnnotationLabelWithPopoverProps {
30
33
  title: string;
31
34
  subtitle?: string;
32
- renderLabel: FC< { title: string; subtitle?: string } >;
33
- renderLabelPopover: FC< { title: string; subtitle?: string } >;
35
+ renderLabel: ( props: { title: string; subtitle?: string } ) => ReactNode;
36
+ renderLabelPopover: ( props: { title: string; subtitle?: string } ) => ReactNode;
34
37
  }
35
38
 
36
39
  const LineChartAnnotationLabelWithPopover: FC< LineChartAnnotationLabelWithPopoverProps > = ( {
@@ -83,7 +86,9 @@ const LineChartAnnotationLabelWithPopover: FC< LineChartAnnotationLabelWithPopov
83
86
  <div className={ styles[ 'line-chart__annotation-label' ] }>
84
87
  <button
85
88
  ref={ buttonRef }
86
- { ...( { popovertarget: popoverId } as ButtonWithPopover ) }
89
+ { ...( isReact18
90
+ ? ( { popovertarget: popoverId } as ButtonWithPopover )
91
+ : { popoverTarget: popoverId } ) }
87
92
  className={ styles[ 'line-chart__annotation-label-trigger-button' ] }
88
93
  style={ {
89
94
  width: `${ POPOVER_BUTTON_SIZE }px`,
@@ -110,10 +115,15 @@ const LineChartAnnotationLabelWithPopover: FC< LineChartAnnotationLabelWithPopov
110
115
  { renderLabelPopover( { title, subtitle } ) }
111
116
  </div>
112
117
  <button
113
- { ...( {
114
- popovertarget: popoverId,
115
- popovertargetaction: 'hide',
116
- } as ButtonWithPopover ) }
118
+ { ...( isReact18
119
+ ? ( {
120
+ popovertarget: popoverId,
121
+ popovertargetaction: 'hide',
122
+ } as ButtonWithPopover )
123
+ : {
124
+ popoverTarget: popoverId,
125
+ popoverTargetAction: 'hide',
126
+ } ) }
117
127
  className={ styles[ 'line-chart__annotation-label-popover-close-button' ] }
118
128
  aria-label={ __( 'Close', 'jetpack-charts' ) }
119
129
  >
@@ -66,7 +66,7 @@ describe( 'renderDefaultTooltip date heading', () => {
66
66
  </GlobalChartsProvider>
67
67
  );
68
68
 
69
- screen.getByRole( 'grid', { name: /line chart/i } ).focus();
69
+ screen.getByRole( 'application', { name: /line chart/i } ).focus();
70
70
  await user.keyboard( '{ArrowRight}' );
71
71
 
72
72
  expect( screen.getByTestId( 'chart-tooltip-0' ) ).toHaveTextContent( '3.8.2026' );