@automattic/charts 4.0.0 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/SECURITY.md +1 -0
- package/TOKENS.md +7 -5
- package/dist/index.cjs +614 -259
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +30 -0
- package/dist/index.d.cts +88 -74
- package/dist/index.d.ts +88 -74
- package/dist/index.js +615 -261
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
- package/src/charts/area-chart/area-chart.tsx +2 -1
- package/src/charts/area-chart/test/area-chart.test.tsx +31 -0
- package/src/charts/heatmap-chart/build-calendar-data.ts +189 -0
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +31 -0
- package/src/charts/heatmap-chart/heatmap-chart.tsx +44 -9
- package/src/charts/heatmap-chart/index.ts +10 -3
- package/src/charts/heatmap-chart/private/civil-day.ts +174 -0
- package/src/charts/heatmap-chart/private/index.ts +0 -2
- package/src/charts/heatmap-chart/private/use-heatmap-colors.ts +5 -1
- package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +4 -1
- package/src/charts/heatmap-chart/test/civil-day.test.ts +70 -0
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +91 -0
- package/src/charts/heatmap-chart/test/use-calendar-heatmap-data.test.tsx +118 -0
- package/src/charts/heatmap-chart/test/use-heatmap-colors.test.ts +8 -0
- package/src/charts/heatmap-chart/test/zoned-calendar-data.test.ts +248 -0
- package/src/charts/heatmap-chart/types.ts +35 -0
- package/src/charts/heatmap-chart/use-calendar-heatmap-data.ts +42 -0
- package/src/charts/leaderboard-chart/types.ts +2 -2
- package/src/charts/line-chart/line-chart.tsx +41 -7
- package/src/charts/line-chart/test/crosshair-style-types.test.ts +31 -0
- package/src/charts/line-chart/test/line-chart.test.tsx +178 -38
- package/src/charts/line-chart/test/zoned-date-string.test.tsx +37 -0
- package/src/charts/line-chart/types.ts +13 -2
- package/src/charts/pie-chart/test/pie-chart.test.tsx +3 -12
- package/src/components/tooltip/accessible-tooltip.tsx +40 -12
- package/src/components/tooltip/private/bounded-tooltip.tsx +71 -16
- package/src/components/tooltip/test/accessible-tooltip.test.tsx +60 -1
- package/src/components/tooltip/test/bounded-tooltip.test.tsx +102 -6
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +122 -3
- package/src/components/tooltip/xy-chart-tooltip.tsx +53 -14
- package/src/hooks/test/use-chart-data-transform-zone.test.tsx +36 -0
- package/src/hooks/test/use-chart-margin.test.tsx +173 -3
- package/src/hooks/use-chart-data-transform.ts +10 -3
- package/src/hooks/use-chart-margin.tsx +54 -9
- package/src/index.ts +4 -0
- package/src/styles/chart-scope.scss +11 -3
- package/src/styles/test/chart-scope.test.ts +35 -1
- package/src/test-utils/zoned-parsing-suite.ts +90 -0
- package/src/types.ts +2 -2
- package/src/utils/date-formatting.ts +1 -18
- package/src/utils/date-parsing.ts +103 -52
- package/src/utils/get-edge-tick-widths.ts +33 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/test/date-parsing.auckland.test.ts +6 -0
- package/src/utils/test/date-parsing.santiago.test.ts +6 -0
- package/src/utils/test/date-parsing.test.ts +16 -1
- package/src/utils/test/get-edge-tick-widths.test.ts +55 -0
- package/src/utils/warn-once.ts +23 -0
- package/src/visx/types.ts +27 -3
- package/src/charts/heatmap-chart/private/build-calendar-data.ts +0 -179
|
@@ -58,7 +58,7 @@ import type { RenderTooltipParams } from '../../visx/types';
|
|
|
58
58
|
import type { ResponsiveConfig } from '../private/with-responsive';
|
|
59
59
|
import type { TickFormatter } from '@visx/axis';
|
|
60
60
|
import type { GlyphProps } from '@visx/xychart';
|
|
61
|
-
import type { FC, Ref } from 'react';
|
|
61
|
+
import type { CSSProperties, FC, Ref } from 'react';
|
|
62
62
|
|
|
63
63
|
const defaultRenderGlyph = < Datum extends object >( props: RenderLineGlyphProps< Datum > ) => {
|
|
64
64
|
return <DefaultGlyph { ...props } key={ props.key } />;
|
|
@@ -108,11 +108,13 @@ const TooltipDate: FC< { date?: Date; displayResolution: Exclude< TickResolution
|
|
|
108
108
|
* one row per visible series (label + formatted value), sorted descending by
|
|
109
109
|
* value. Reused by AreaChart, which has the same multi-series shape.
|
|
110
110
|
*
|
|
111
|
-
* @param params
|
|
111
|
+
* @param params - visx tooltip data and the chart's optional `bucketInfo`.
|
|
112
|
+
* @param contentStyle - Explicit tooltip content color overrides.
|
|
112
113
|
* @return Tooltip JSX, or `null` when no datum is hovered.
|
|
113
114
|
*/
|
|
114
115
|
export const renderDefaultTooltip = (
|
|
115
|
-
params: RenderTooltipParams< DataPointDate > & { bucketInfo?: BucketInfo }
|
|
116
|
+
params: RenderTooltipParams< DataPointDate > & { bucketInfo?: BucketInfo },
|
|
117
|
+
contentStyle?: Pick< CSSProperties, 'color' | 'background' | 'backgroundColor' >
|
|
116
118
|
) => {
|
|
117
119
|
const { tooltipData, bucketInfo } = params;
|
|
118
120
|
const nearestDatum = tooltipData?.nearestDatum?.datum;
|
|
@@ -126,7 +128,11 @@ export const renderDefaultTooltip = (
|
|
|
126
128
|
.sort( ( a, b ) => b.value - a.value );
|
|
127
129
|
|
|
128
130
|
return (
|
|
129
|
-
<div
|
|
131
|
+
<div
|
|
132
|
+
className={ styles[ 'line-chart__tooltip' ] }
|
|
133
|
+
data-testid="line-chart-tooltip-content"
|
|
134
|
+
style={ contentStyle }
|
|
135
|
+
>
|
|
130
136
|
<div className={ styles[ 'line-chart__tooltip-date' ] }>
|
|
131
137
|
<TooltipDate
|
|
132
138
|
date={ nearestDatum.date }
|
|
@@ -221,6 +227,8 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
221
227
|
smoothing = true,
|
|
222
228
|
curveType,
|
|
223
229
|
renderTooltip = renderDefaultTooltip,
|
|
230
|
+
tooltipPlacement,
|
|
231
|
+
tooltipStyle,
|
|
224
232
|
withStartGlyphs = false,
|
|
225
233
|
withEndGlyphs = false,
|
|
226
234
|
animation,
|
|
@@ -354,6 +362,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
354
362
|
chartRef,
|
|
355
363
|
totalPoints: dataSorted[ 0 ]?.data.length || 0,
|
|
356
364
|
onActivate: activateSelectedPoint,
|
|
365
|
+
preventTooltipScroll: tooltipPlacement === 'below-axis',
|
|
357
366
|
} );
|
|
358
367
|
|
|
359
368
|
const chartOptions = useMemo( () => {
|
|
@@ -473,12 +482,32 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
473
482
|
yAccessor: ( d: DataPointDate ) => d?.value,
|
|
474
483
|
};
|
|
475
484
|
|
|
485
|
+
const resolvedTooltipStyle = useMemo( () => {
|
|
486
|
+
if ( renderTooltip !== renderDefaultTooltip || ! tooltipStyle ) return tooltipStyle;
|
|
487
|
+
if ( ! tooltipStyle.color || tooltipStyle.background || tooltipStyle.backgroundColor ) {
|
|
488
|
+
return tooltipStyle;
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
backgroundColor: 'var(--a8c-charts-color-tooltip-surface, rgb(0 0 0 / 85%))',
|
|
492
|
+
...tooltipStyle,
|
|
493
|
+
};
|
|
494
|
+
}, [ renderTooltip, tooltipStyle ] );
|
|
495
|
+
|
|
476
496
|
// Augments every renderTooltip call with the chart's bucket classification,
|
|
477
497
|
// default or custom, so a heading keyed on it can't disagree with the axis.
|
|
478
498
|
const tooltipRenderer = useMemo(
|
|
479
499
|
() => ( params: RenderTooltipParams< DataPointDate > ) =>
|
|
480
|
-
renderTooltip
|
|
481
|
-
|
|
500
|
+
renderTooltip === renderDefaultTooltip
|
|
501
|
+
? renderDefaultTooltip(
|
|
502
|
+
{ ...params, bucketInfo },
|
|
503
|
+
{
|
|
504
|
+
color: tooltipStyle?.color,
|
|
505
|
+
background: tooltipStyle?.background,
|
|
506
|
+
backgroundColor: tooltipStyle?.backgroundColor,
|
|
507
|
+
}
|
|
508
|
+
)
|
|
509
|
+
: renderTooltip( { ...params, bucketInfo } ),
|
|
510
|
+
[ renderTooltip, bucketInfo, tooltipStyle ]
|
|
482
511
|
);
|
|
483
512
|
|
|
484
513
|
if ( error ) {
|
|
@@ -533,6 +562,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
533
562
|
|
|
534
563
|
return (
|
|
535
564
|
<div
|
|
565
|
+
ref={ chartRef }
|
|
536
566
|
role="grid"
|
|
537
567
|
aria-label={ __( 'Line chart', 'jetpack-charts' ) }
|
|
538
568
|
tabIndex={ 0 }
|
|
@@ -541,7 +571,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
541
571
|
onBlur={ onChartBlur }
|
|
542
572
|
>
|
|
543
573
|
{ chartHeight > 0 && (
|
|
544
|
-
<div
|
|
574
|
+
<div className={ plotStyles[ 'xy-plot' ] }>
|
|
545
575
|
{ zoomable && zoom.domain && <ZoomResetButton onClick={ zoom.reset } /> }
|
|
546
576
|
<XYChart
|
|
547
577
|
theme={ theme }
|
|
@@ -676,6 +706,8 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
676
706
|
<AccessibleTooltip
|
|
677
707
|
detectBounds
|
|
678
708
|
snapTooltipToDatumX
|
|
709
|
+
tooltipPlacement={ tooltipPlacement }
|
|
710
|
+
style={ resolvedTooltipStyle }
|
|
679
711
|
snapTooltipToDatumY
|
|
680
712
|
showSeriesGlyphs
|
|
681
713
|
renderTooltip={ tooltipRenderer }
|
|
@@ -683,6 +715,8 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
683
715
|
glyphStyle={ glyphStyle }
|
|
684
716
|
showVerticalCrosshair={ withTooltipCrosshairs?.showVertical }
|
|
685
717
|
showHorizontalCrosshair={ withTooltipCrosshairs?.showHorizontal }
|
|
718
|
+
verticalCrosshairStyle={ withTooltipCrosshairs?.verticalStyle }
|
|
719
|
+
horizontalCrosshairStyle={ withTooltipCrosshairs?.horizontalStyle }
|
|
686
720
|
selectedIndex={ selectedIndex }
|
|
687
721
|
tooltipRef={ tooltipRef }
|
|
688
722
|
keyboardFocusedClassName={
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { LineChartProps } from '../types';
|
|
2
|
+
|
|
3
|
+
type Crosshairs = NonNullable< LineChartProps[ 'withTooltipCrosshairs' ] >;
|
|
4
|
+
|
|
5
|
+
describe( 'line chart crosshair style types', () => {
|
|
6
|
+
test( 'accepts paint attributes', () => {
|
|
7
|
+
const crosshairs: Crosshairs = { verticalStyle: { stroke: 'purple', strokeWidth: 2 } };
|
|
8
|
+
|
|
9
|
+
expect( crosshairs.verticalStyle?.stroke ).toBe( 'purple' );
|
|
10
|
+
} );
|
|
11
|
+
|
|
12
|
+
test( 'reserves geometry attributes for the chart', () => {
|
|
13
|
+
const crosshairs: Crosshairs = {
|
|
14
|
+
// @ts-expect-error Crosshair coordinates are computed from the active datum.
|
|
15
|
+
verticalStyle: { x1: 12 },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const transformedCrosshairs: Crosshairs = {
|
|
19
|
+
// @ts-expect-error Crosshair transforms are not paint attributes.
|
|
20
|
+
verticalStyle: { transform: 'translate(100 0)' },
|
|
21
|
+
};
|
|
22
|
+
const transformedStyles: Crosshairs = {
|
|
23
|
+
// @ts-expect-error Inline crosshair styles accept only paint properties.
|
|
24
|
+
horizontalStyle: { style: { transform: 'translateX(100px)' } },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
expect( crosshairs ).toBeDefined();
|
|
28
|
+
expect( transformedCrosshairs ).toBeDefined();
|
|
29
|
+
expect( transformedStyles ).toBeDefined();
|
|
30
|
+
} );
|
|
31
|
+
} );
|
|
@@ -104,6 +104,137 @@ describe( 'LineChart', () => {
|
|
|
104
104
|
);
|
|
105
105
|
};
|
|
106
106
|
|
|
107
|
+
test.each( [ 'auto', 'below-axis' ] as const )(
|
|
108
|
+
'passes custom tooltip container and crosshair styles with %s placement',
|
|
109
|
+
async tooltipPlacement => {
|
|
110
|
+
const user = userEvent.setup();
|
|
111
|
+
renderWithTheme( {
|
|
112
|
+
withTooltipCrosshairs: {
|
|
113
|
+
showVertical: true,
|
|
114
|
+
showHorizontal: true,
|
|
115
|
+
verticalStyle: { stroke: 'purple', strokeWidth: 40, strokeOpacity: 0.12 },
|
|
116
|
+
horizontalStyle: { stroke: 'orange', strokeDasharray: '4 2' },
|
|
117
|
+
},
|
|
118
|
+
tooltipPlacement,
|
|
119
|
+
tooltipStyle: { backgroundColor: 'black', color: 'white', boxShadow: 'none' },
|
|
120
|
+
} );
|
|
121
|
+
screen.getByRole( 'grid', { name: /line chart/i } ).focus();
|
|
122
|
+
await user.keyboard( '{ArrowRight}' );
|
|
123
|
+
|
|
124
|
+
const vertical = screen.getByTestId( 'xy-chart-tooltip-crosshair-vertical' );
|
|
125
|
+
expect( vertical ).toHaveAttribute( 'stroke', 'purple' );
|
|
126
|
+
expect( vertical ).toHaveAttribute( 'stroke-width', '40' );
|
|
127
|
+
expect( vertical ).toHaveAttribute( 'stroke-opacity', '0.12' );
|
|
128
|
+
const horizontal = screen.getByTestId( 'xy-chart-tooltip-crosshair-horizontal' );
|
|
129
|
+
expect( horizontal ).toHaveAttribute( 'stroke', 'orange' );
|
|
130
|
+
expect( horizontal ).toHaveAttribute( 'stroke-dasharray', '4 2' );
|
|
131
|
+
expect( screen.getByTestId( 'bounded-tooltip' ) ).toHaveStyle( {
|
|
132
|
+
'background-color': 'rgb(0, 0, 0)',
|
|
133
|
+
color: 'rgb(255, 255, 255)',
|
|
134
|
+
'box-shadow': 'none',
|
|
135
|
+
} );
|
|
136
|
+
expect( screen.getByTestId( 'line-chart-tooltip-content' ) ).toHaveStyle( {
|
|
137
|
+
color: 'rgb(255, 255, 255)',
|
|
138
|
+
} );
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
test.each( [
|
|
142
|
+
[
|
|
143
|
+
{ color: 'white' },
|
|
144
|
+
'rgb(255, 255, 255)',
|
|
145
|
+
'var(--a8c-charts-color-tooltip-surface, rgb(0 0 0 / 85%))',
|
|
146
|
+
{ color: 'rgb(255, 255, 255)' },
|
|
147
|
+
],
|
|
148
|
+
[ { backgroundColor: 'white' }, 'var(--a8c-charts-color-label)', 'rgb(255, 255, 255)', {} ],
|
|
149
|
+
[ { background: 'white' }, 'var(--a8c-charts-color-label)', 'white', {} ],
|
|
150
|
+
[
|
|
151
|
+
{ color: 'white', background: 'black' },
|
|
152
|
+
'rgb(255, 255, 255)',
|
|
153
|
+
'black',
|
|
154
|
+
{ color: 'rgb(255, 255, 255)' },
|
|
155
|
+
],
|
|
156
|
+
] )(
|
|
157
|
+
'applies directional tooltip color defaults: %j',
|
|
158
|
+
async ( tooltipStyle, color, background, containerColorStyle ) => {
|
|
159
|
+
const user = userEvent.setup();
|
|
160
|
+
const defaults = document.createElement( 'style' );
|
|
161
|
+
defaults.textContent = `.line-chart__tooltip {
|
|
162
|
+
color: var(--a8c-charts-color-label);
|
|
163
|
+
background: var(--a8c-charts-color-surface);
|
|
164
|
+
}`;
|
|
165
|
+
renderWithTheme( { tooltipStyle } );
|
|
166
|
+
const chart = screen.getByTestId( 'line-chart' );
|
|
167
|
+
chart.appendChild( defaults );
|
|
168
|
+
chart.style.setProperty( '--a8c-charts-color-label', '#000' );
|
|
169
|
+
chart.style.setProperty( '--a8c-charts-color-label-axis', '#aaa' );
|
|
170
|
+
chart.style.setProperty( '--a8c-charts-color-surface', '#fff' );
|
|
171
|
+
screen.getByRole( 'grid', { name: /line chart/i } ).focus();
|
|
172
|
+
await user.keyboard( '{ArrowRight}' );
|
|
173
|
+
|
|
174
|
+
const content = screen.getByTestId( 'line-chart-tooltip-content' );
|
|
175
|
+
expect( content ).toHaveStyle( {
|
|
176
|
+
color,
|
|
177
|
+
[ 'backgroundColor' in tooltipStyle ? 'background-color' : 'background' ]:
|
|
178
|
+
'background' in tooltipStyle || 'backgroundColor' in tooltipStyle
|
|
179
|
+
? background
|
|
180
|
+
: 'var(--a8c-charts-color-surface)',
|
|
181
|
+
} );
|
|
182
|
+
const container = screen.getByTestId( 'bounded-tooltip' );
|
|
183
|
+
expect( container ).toHaveStyle( {
|
|
184
|
+
...containerColorStyle,
|
|
185
|
+
[ 'background' in tooltipStyle ? 'background' : 'background-color' ]: background,
|
|
186
|
+
} );
|
|
187
|
+
}
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
test( 'preserves the existing tooltip colors for noncolor overrides', async () => {
|
|
191
|
+
const user = userEvent.setup();
|
|
192
|
+
renderWithTheme( { tooltipStyle: { boxShadow: 'none' } } );
|
|
193
|
+
screen.getByRole( 'grid', { name: /line chart/i } ).focus();
|
|
194
|
+
await user.keyboard( '{ArrowRight}' );
|
|
195
|
+
|
|
196
|
+
const content = screen.getByTestId( 'line-chart-tooltip-content' );
|
|
197
|
+
expect( content.style.getPropertyValue( 'color' ) ).toBe( '' );
|
|
198
|
+
expect( content.style.getPropertyValue( 'background' ) ).toBe( '' );
|
|
199
|
+
const container = screen.getByTestId( 'bounded-tooltip' );
|
|
200
|
+
expect( container ).toHaveStyle( { 'box-shadow': 'none' } );
|
|
201
|
+
expect( container.style.getPropertyValue( 'background' ) ).toBe( '' );
|
|
202
|
+
expect( container.style.getPropertyValue( 'color' ) ).not.toBe(
|
|
203
|
+
'var(--a8c-charts-color-label-inverse)'
|
|
204
|
+
);
|
|
205
|
+
} );
|
|
206
|
+
|
|
207
|
+
test.each( [ [ 'Escape', '{Escape}' ] ] )(
|
|
208
|
+
'returns focus to the grid after %s',
|
|
209
|
+
async ( _name, keys ) => {
|
|
210
|
+
jest.useFakeTimers();
|
|
211
|
+
try {
|
|
212
|
+
const user = userEvent.setup( { advanceTimers: jest.advanceTimersByTime } );
|
|
213
|
+
renderWithTheme();
|
|
214
|
+
const chart = screen.getByRole( 'grid', { name: /line chart/i } );
|
|
215
|
+
|
|
216
|
+
await user.tab();
|
|
217
|
+
expect( chart ).toHaveFocus();
|
|
218
|
+
await user.keyboard( '{ArrowRight}' );
|
|
219
|
+
expect( screen.getByRole( 'tooltip' ) ).toHaveFocus();
|
|
220
|
+
|
|
221
|
+
await user.keyboard( keys );
|
|
222
|
+
await act( async () => {
|
|
223
|
+
jest.advanceTimersByTime( 5000 );
|
|
224
|
+
} );
|
|
225
|
+
expect( chart ).toHaveFocus();
|
|
226
|
+
expect( screen.queryByRole( 'tooltip' ) ).not.toBeInTheDocument();
|
|
227
|
+
|
|
228
|
+
await user.keyboard( '{ArrowRight}' );
|
|
229
|
+
const tooltip = screen.getByRole( 'tooltip' );
|
|
230
|
+
expect( tooltip ).toHaveFocus();
|
|
231
|
+
expect( tooltip ).toHaveTextContent( 'Series A:10' );
|
|
232
|
+
} finally {
|
|
233
|
+
jest.useRealTimers();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
);
|
|
237
|
+
|
|
107
238
|
describe( 'Data Validation', () => {
|
|
108
239
|
test( 'handles empty data array', () => {
|
|
109
240
|
renderWithTheme( { data: [] } );
|
|
@@ -1225,44 +1356,53 @@ describe( 'LineChart', () => {
|
|
|
1225
1356
|
} );
|
|
1226
1357
|
|
|
1227
1358
|
describe( 'Arrow Key Navigation', () => {
|
|
1228
|
-
test( '
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1359
|
+
test.each( [ 'auto', 'below-axis' ] as const )(
|
|
1360
|
+
'right arrow key navigates with %s tooltips',
|
|
1361
|
+
async tooltipPlacement => {
|
|
1362
|
+
const user = userEvent.setup();
|
|
1363
|
+
renderWithTheme( {
|
|
1364
|
+
tooltipPlacement,
|
|
1365
|
+
data: [
|
|
1366
|
+
{
|
|
1367
|
+
label: 'Series A',
|
|
1368
|
+
data: [
|
|
1369
|
+
{ date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' },
|
|
1370
|
+
{ date: new Date( '2024-01-02' ), value: 20, label: 'Jan 2' },
|
|
1371
|
+
],
|
|
1372
|
+
options: {},
|
|
1373
|
+
},
|
|
1374
|
+
{
|
|
1375
|
+
label: 'Series B',
|
|
1376
|
+
data: [
|
|
1377
|
+
{ date: new Date( '2024-01-01' ), value: 15, label: 'Jan 1' },
|
|
1378
|
+
{ date: new Date( '2024-01-02' ), value: 25, label: 'Jan 2' },
|
|
1379
|
+
],
|
|
1380
|
+
options: {},
|
|
1381
|
+
},
|
|
1382
|
+
],
|
|
1383
|
+
} );
|
|
1384
|
+
|
|
1385
|
+
const chart = screen.getByRole( 'grid', { name: /line chart/i } );
|
|
1386
|
+
chart.focus();
|
|
1387
|
+
|
|
1388
|
+
// Single tab should focus on the first tooltip.
|
|
1389
|
+
await user.keyboard( '{ArrowRight}' );
|
|
1390
|
+
expect( screen.getByTestId( 'chart-tooltip-0' ) ).toHaveFocus();
|
|
1391
|
+
expect( screen.getByTestId( 'chart-tooltip-0' ) ).toHaveAttribute( 'role', 'tooltip' );
|
|
1392
|
+
expect( screen.getByTestId( 'chart-tooltip-0' ) ).toHaveAttribute(
|
|
1393
|
+
'aria-atomic',
|
|
1394
|
+
'true'
|
|
1395
|
+
);
|
|
1396
|
+
expect( screen.getByTestId( 'chart-tooltip-0' ) ).toHaveTextContent( 'Series A' );
|
|
1397
|
+
expect( screen.queryByTestId( 'chart-tooltip-1' ) ).not.toBeInTheDocument();
|
|
1398
|
+
|
|
1399
|
+
// Second tab should focus on the second tooltip.
|
|
1400
|
+
await user.keyboard( '{ArrowRight}' );
|
|
1401
|
+
expect( screen.getByTestId( 'chart-tooltip-1' ) ).toHaveFocus();
|
|
1402
|
+
expect( screen.getByTestId( 'chart-tooltip-1' ) ).toHaveTextContent( 'Series B' );
|
|
1403
|
+
expect( screen.queryByTestId( 'chart-tooltip-0' ) ).not.toBeInTheDocument();
|
|
1404
|
+
}
|
|
1405
|
+
);
|
|
1266
1406
|
|
|
1267
1407
|
test( 'left arrow key navigates to previous data point', async () => {
|
|
1268
1408
|
const user = userEvent.setup();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment <rootDir>/tests/environment-auckland.mjs
|
|
3
|
+
*/
|
|
4
|
+
import { render, screen } from '@testing-library/react';
|
|
5
|
+
import { GlobalChartsProvider } from '../../../providers';
|
|
6
|
+
import LineChart from '../line-chart';
|
|
7
|
+
|
|
8
|
+
const mockRefCallback = jest.fn();
|
|
9
|
+
jest.mock( '../../../hooks/use-element-size', () => ( {
|
|
10
|
+
useElementSize: () => [ mockRefCallback, 500, 300 ],
|
|
11
|
+
} ) );
|
|
12
|
+
|
|
13
|
+
// Naive day strings, the shape the Stats payload gives for day buckets.
|
|
14
|
+
const data = [
|
|
15
|
+
{
|
|
16
|
+
label: 'Series A',
|
|
17
|
+
data: [
|
|
18
|
+
{ dateString: '2026-08-02', value: 1 },
|
|
19
|
+
{ dateString: '2026-08-03', value: 2 },
|
|
20
|
+
{ dateString: '2026-08-04', value: 3 },
|
|
21
|
+
],
|
|
22
|
+
options: {},
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
describe( 'dateString under a host time zone', () => {
|
|
27
|
+
it( 'dates a naive string in the host zone, not the viewer browser zone', () => {
|
|
28
|
+
render(
|
|
29
|
+
<GlobalChartsProvider locale="en-US" timeZone="Asia/Tokyo">
|
|
30
|
+
<LineChart width={ 500 } height={ 300 } withGradientFill={ false } data={ data } />
|
|
31
|
+
</GlobalChartsProvider>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const ticks = screen.getAllByText( /^[A-Z][a-z]{2} \d{1,2}$/ );
|
|
35
|
+
expect( ticks.map( tick => tick.textContent ) ).toEqual( [ 'Aug 2', 'Aug 3', 'Aug 4' ] );
|
|
36
|
+
} );
|
|
37
|
+
} );
|
|
@@ -8,9 +8,9 @@ import type {
|
|
|
8
8
|
AnnotationStyles,
|
|
9
9
|
DataPoint,
|
|
10
10
|
} from '../../types';
|
|
11
|
-
import type { RenderTooltipParams } from '../../visx/types';
|
|
11
|
+
import type { CrosshairStyle, RenderTooltipParams, XyChartTooltipProps } from '../../visx/types';
|
|
12
12
|
import type { GlyphProps } from '@visx/xychart';
|
|
13
|
-
import type { ReactNode, SVGProps, FC } from 'react';
|
|
13
|
+
import type { ReactNode, SVGProps, FC, CSSProperties } from 'react';
|
|
14
14
|
|
|
15
15
|
export type LineChartAnnotationProps = {
|
|
16
16
|
datum: DataPointDate;
|
|
@@ -41,6 +41,15 @@ export interface LineChartProps extends BaseChartProps< SeriesData[] >, SeriesVi
|
|
|
41
41
|
renderTooltip?: (
|
|
42
42
|
params: RenderTooltipParams< DataPointDate > & { bucketInfo?: BucketInfo }
|
|
43
43
|
) => ReactNode;
|
|
44
|
+
/**
|
|
45
|
+
* Place the panel below the x-axis tick labels with a pointer at the datum x; horizontal bounds still apply.
|
|
46
|
+
* @default 'auto'
|
|
47
|
+
*/
|
|
48
|
+
tooltipPlacement?: XyChartTooltipProps< DataPointDate >[ 'tooltipPlacement' ];
|
|
49
|
+
/**
|
|
50
|
+
* Inline container styles; see Below-Axis Tooltips in stories/index.docs.mdx for content color overrides.
|
|
51
|
+
*/
|
|
52
|
+
tooltipStyle?: CSSProperties;
|
|
44
53
|
withStartGlyphs?: boolean;
|
|
45
54
|
withEndGlyphs?: boolean;
|
|
46
55
|
renderGlyph?: < Datum extends object >( props: GlyphProps< Datum > ) => ReactNode;
|
|
@@ -49,6 +58,8 @@ export interface LineChartProps extends BaseChartProps< SeriesData[] >, SeriesVi
|
|
|
49
58
|
withTooltipCrosshairs?: {
|
|
50
59
|
showVertical?: boolean;
|
|
51
60
|
showHorizontal?: boolean;
|
|
61
|
+
verticalStyle?: CrosshairStyle;
|
|
62
|
+
horizontalStyle?: CrosshairStyle;
|
|
52
63
|
};
|
|
53
64
|
/**
|
|
54
65
|
* Enable drag-to-zoom on the X axis. The user drags horizontally to
|
|
@@ -123,18 +123,9 @@ describe( 'PieChart', () => {
|
|
|
123
123
|
test( 'hides labels when showLabels is false', () => {
|
|
124
124
|
renderWithTheme( { showLabels: false } );
|
|
125
125
|
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
// Check if this text element is not the measurement element
|
|
130
|
-
return (
|
|
131
|
-
( content === 'A' || content === 'B' ) &&
|
|
132
|
-
element?.id !== '__react_svg_text_measurement_id'
|
|
133
|
-
);
|
|
134
|
-
} );
|
|
135
|
-
|
|
136
|
-
// Labels should not be present in the rendered output (excluding measurement text)
|
|
137
|
-
expect( labelElements ).toHaveLength( 0 );
|
|
126
|
+
// A plain query, so this also fails if the measurement node stops being
|
|
127
|
+
// ignored — see tests/setup-text-measurement.js.
|
|
128
|
+
expect( screen.queryAllByText( /^[AB]$/ ) ).toHaveLength( 0 );
|
|
138
129
|
} );
|
|
139
130
|
|
|
140
131
|
test( 'shows labels when showLabels is explicitly true', () => {
|
|
@@ -200,6 +200,7 @@ interface UseKeyboardNavigationProps {
|
|
|
200
200
|
* keyboard selection the way it treats a click.
|
|
201
201
|
*/
|
|
202
202
|
onActivate?: ( index: number ) => void;
|
|
203
|
+
preventTooltipScroll?: boolean;
|
|
203
204
|
}
|
|
204
205
|
|
|
205
206
|
export const useKeyboardNavigation = ( {
|
|
@@ -210,23 +211,34 @@ export const useKeyboardNavigation = ( {
|
|
|
210
211
|
chartRef,
|
|
211
212
|
totalPoints,
|
|
212
213
|
onActivate,
|
|
214
|
+
preventTooltipScroll = false,
|
|
213
215
|
}: UseKeyboardNavigationProps ) => {
|
|
214
216
|
// Focus the tooltip as soon as it is rendered
|
|
215
217
|
const tooltipRef = useCallback(
|
|
216
218
|
( element: HTMLDivElement | null ) => {
|
|
217
219
|
if ( element && selectedIndex !== undefined ) {
|
|
218
|
-
|
|
220
|
+
if ( preventTooltipScroll ) {
|
|
221
|
+
element.focus( { preventScroll: true } );
|
|
222
|
+
} else {
|
|
223
|
+
element.focus();
|
|
224
|
+
}
|
|
219
225
|
}
|
|
220
226
|
},
|
|
221
|
-
[ selectedIndex ]
|
|
227
|
+
[ preventTooltipScroll, selectedIndex ]
|
|
222
228
|
);
|
|
223
229
|
|
|
224
|
-
//
|
|
225
|
-
const onChartFocus = useCallback(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
+
// Returning focus from the tooltip must not restore the selection Escape just cleared.
|
|
231
|
+
const onChartFocus = useCallback(
|
|
232
|
+
( event: React.FocusEvent< HTMLDivElement > ) => {
|
|
233
|
+
if ( event.currentTarget.contains( event.relatedTarget ) ) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if ( ! isNavigating && selectedIndex !== undefined ) {
|
|
237
|
+
setSelectedIndex( 0 );
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
[ isNavigating, selectedIndex, setSelectedIndex ]
|
|
241
|
+
);
|
|
230
242
|
|
|
231
243
|
// On each blur of chart, keyboard navigation should restart from first tooltip
|
|
232
244
|
const onChartBlur = useCallback( () => {
|
|
@@ -237,9 +249,17 @@ export const useKeyboardNavigation = ( {
|
|
|
237
249
|
( event: React.KeyboardEvent< HTMLDivElement > ) => {
|
|
238
250
|
if ( totalPoints === 0 ) return;
|
|
239
251
|
|
|
252
|
+
const focusChart = () => {
|
|
253
|
+
if ( preventTooltipScroll ) {
|
|
254
|
+
chartRef.current?.focus( { preventScroll: true } );
|
|
255
|
+
} else {
|
|
256
|
+
chartRef.current?.focus();
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
240
260
|
// Keep focus on the chart if tab is pressed
|
|
241
261
|
if ( event.key === 'Tab' ) {
|
|
242
|
-
|
|
262
|
+
focusChart();
|
|
243
263
|
setSelectedIndex( undefined );
|
|
244
264
|
setIsNavigating( false );
|
|
245
265
|
return;
|
|
@@ -248,7 +268,7 @@ export const useKeyboardNavigation = ( {
|
|
|
248
268
|
const currentSelectedIndex = selectedIndex === undefined ? -1 : selectedIndex;
|
|
249
269
|
|
|
250
270
|
if ( currentSelectedIndex + 1 >= totalPoints && [ 'ArrowRight' ].includes( event.key ) ) {
|
|
251
|
-
|
|
271
|
+
focusChart();
|
|
252
272
|
setSelectedIndex( undefined );
|
|
253
273
|
setIsNavigating( false );
|
|
254
274
|
return;
|
|
@@ -265,12 +285,20 @@ export const useKeyboardNavigation = ( {
|
|
|
265
285
|
} else if ( event.key === 'Escape' ) {
|
|
266
286
|
setSelectedIndex( undefined );
|
|
267
287
|
setIsNavigating( false );
|
|
268
|
-
|
|
288
|
+
focusChart();
|
|
269
289
|
} else if ( ( event.key === 'Enter' || event.key === ' ' ) && selectedIndex !== undefined ) {
|
|
270
290
|
onActivate?.( selectedIndex );
|
|
271
291
|
}
|
|
272
292
|
},
|
|
273
|
-
[
|
|
293
|
+
[
|
|
294
|
+
totalPoints,
|
|
295
|
+
selectedIndex,
|
|
296
|
+
setSelectedIndex,
|
|
297
|
+
setIsNavigating,
|
|
298
|
+
chartRef,
|
|
299
|
+
onActivate,
|
|
300
|
+
preventTooltipScroll,
|
|
301
|
+
]
|
|
274
302
|
);
|
|
275
303
|
|
|
276
304
|
return {
|