@automattic/charts 4.7.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.
- package/CHANGELOG.md +30 -0
- package/TOKENS.md +6 -3
- package/dist/index.cjs +834 -321
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +68 -17
- package/dist/index.d.cts +102 -28
- package/dist/index.d.ts +102 -28
- package/dist/index.js +838 -327
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.tsx +7 -4
- package/src/charts/area-chart/test/area-chart.test.tsx +24 -15
- package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +1 -1
- package/src/charts/area-chart/test/tooltip-resolution.test.tsx +2 -2
- package/src/charts/area-chart/types.ts +2 -0
- package/src/charts/bar-chart/bar-chart.module.scss +7 -0
- package/src/charts/bar-chart/bar-chart.tsx +25 -16
- package/src/charts/bar-chart/private/comparison-bars.tsx +1 -0
- package/src/charts/bar-chart/test/bar-chart.test.tsx +106 -62
- package/src/charts/bar-chart/types.ts +2 -0
- package/src/charts/bar-list-chart/test/bar-list-chart.test.tsx +3 -1
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +1 -1
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +9 -0
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +16 -8
- package/src/charts/heatmap-chart/heatmap-chart.tsx +183 -66
- package/src/charts/heatmap-chart/private/column-groups.ts +79 -8
- package/src/charts/heatmap-chart/private/fit-compact-cells.ts +72 -0
- package/src/charts/heatmap-chart/private/heatmap-context.ts +2 -2
- package/src/charts/heatmap-chart/private/heatmap-legend.tsx +15 -3
- package/src/charts/heatmap-chart/private/heatmap-scale.ts +74 -0
- package/src/charts/heatmap-chart/private/index.ts +6 -3
- package/src/charts/heatmap-chart/private/use-heatmap-colors.ts +24 -6
- package/src/charts/heatmap-chart/test/column-groups.test.ts +41 -4
- package/src/charts/heatmap-chart/test/fit-compact-cells.test.ts +59 -0
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +231 -8
- package/src/charts/heatmap-chart/test/heatmap-scale.test.ts +78 -0
- package/src/charts/heatmap-chart/test/use-heatmap-colors.test.ts +18 -6
- package/src/charts/heatmap-chart/types.ts +6 -3
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +46 -4
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +37 -1
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +53 -0
- package/src/charts/leaderboard-chart/types.ts +6 -1
- package/src/charts/line-chart/line-chart.tsx +7 -4
- package/src/charts/line-chart/private/line-chart-annotation-label-popover.tsx +19 -9
- package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +1 -1
- package/src/charts/line-chart/test/line-chart.test.tsx +45 -35
- package/src/charts/line-chart/test/tooltip-resolution.test.tsx +2 -2
- package/src/charts/line-chart/types.ts +5 -3
- package/src/charts/pie-chart/pie-chart.tsx +153 -143
- package/src/charts/pie-chart/test/pie-chart.test.tsx +171 -0
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +126 -124
- package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +55 -1
- package/src/charts/private/chart-composition/use-chart-children.ts +11 -6
- package/src/charts/private/pie-keyboard-navigation/index.ts +4 -0
- package/src/charts/private/pie-keyboard-navigation/order-arcs-for-navigation.ts +10 -0
- package/src/charts/private/pie-keyboard-navigation/pie-keyboard-navigation.module.scss +16 -0
- package/src/charts/private/pie-keyboard-navigation/pie-selection-announcement.tsx +14 -0
- package/src/charts/private/pie-keyboard-navigation/pie-selection-output.tsx +82 -0
- package/src/charts/private/pie-keyboard-navigation/pie-tooltip.tsx +44 -0
- package/src/charts/private/pie-keyboard-navigation/selected-segment-ring.tsx +47 -0
- package/src/charts/private/pie-keyboard-navigation/test/order-arcs-for-navigation.test.ts +34 -0
- package/src/charts/private/pie-keyboard-navigation/test/selected-segment-ring.test.ts +14 -0
- package/src/charts/private/pie-keyboard-navigation/use-pie-keyboard-navigation.ts +139 -0
- package/src/charts/private/test/label-text-color.test.ts +6 -8
- package/src/charts/private/x-zoom/test/x-zoom.test.tsx +3 -2
- package/src/components/tooltip/base-tooltip.tsx +31 -32
- package/src/components/tooltip/index.ts +3 -1
- package/src/components/tooltip/private/bounded-tooltip.tsx +15 -15
- package/src/components/tooltip/private/label-value-content.tsx +8 -0
- package/src/components/tooltip/private/xy-chart-tooltip-overlay.tsx +336 -0
- package/src/components/tooltip/test/tool-tip.test.tsx +8 -6
- package/src/components/tooltip/test/tooltip-box.test.tsx +53 -0
- package/src/components/tooltip/test/xy-chart-tooltip-overlay.test.tsx +445 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +283 -365
- package/src/components/tooltip/{base-tooltip.module.scss → tooltip-box.module.scss} +5 -8
- package/src/components/tooltip/tooltip-box.tsx +43 -0
- package/src/components/tooltip/types.ts +7 -1
- package/src/components/tooltip/xy-chart-tooltip.tsx +410 -300
- package/src/hooks/test/use-chart-margin.test.tsx +8 -4
- package/src/hooks/use-chart-margin.tsx +21 -18
- package/src/index.ts +8 -3
- package/src/providers/chart-context/global-charts-provider.tsx +5 -14
- package/src/providers/chart-context/hooks/use-default-hidden-series.ts +1 -1
- package/src/providers/chart-context/private/catalog-pointers.ts +1 -0
- package/src/providers/chart-context/private/resolve-opaque-hex.ts +16 -0
- package/src/providers/chart-context/test/use-default-hidden-series.test.tsx +1 -1
- package/src/styles/chart-scope.scss +1 -0
- package/src/test-utils/zoned-parsing-suite.ts +0 -10
- package/src/types.ts +18 -2
- package/src/utils/test/date-formatting.test.ts +0 -10
- package/src/components/tooltip/accessible-tooltip.tsx +0 -433
- package/src/components/tooltip/test/accessible-tooltip.test.tsx +0 -331
|
@@ -1,433 +0,0 @@
|
|
|
1
|
-
import { TooltipContext } from '@visx/xychart';
|
|
2
|
-
import clsx from 'clsx';
|
|
3
|
-
import { useContext, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
4
|
-
import { ChartInstanceContext } from '../../charts/private/chart-instance-context';
|
|
5
|
-
import { useGlobalChartsContext } from '../../providers/chart-context/hooks/use-global-charts-context';
|
|
6
|
-
import { CATALOG_POINTERS } from '../../providers/chart-context/private/catalog-pointers';
|
|
7
|
-
import { useChartScopeElement, useStandaloneScopeClass } from '../../providers/chart-scope';
|
|
8
|
-
import { resolveCssVariable } from '../../utils';
|
|
9
|
-
import { XyChartTooltip } from './xy-chart-tooltip';
|
|
10
|
-
import type { SeriesData, DataPointDate } from '../../types';
|
|
11
|
-
import type { RenderTooltipParams, XyChartTooltipProps } from '../../visx/types';
|
|
12
|
-
import type { ReactNode } from 'react';
|
|
13
|
-
|
|
14
|
-
// Type for flattened tooltip data used in individual mode
|
|
15
|
-
export type FlattenedTooltipData = {
|
|
16
|
-
datum: DataPointDate;
|
|
17
|
-
seriesLabel: string;
|
|
18
|
-
seriesIndex: number;
|
|
19
|
-
dataPointIndex: number;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
// The XY chart tooltip, as LineChart, AreaChart and BarChart render it: place it inside a visx `XYChart`, with `useKeyboardNavigation` for keyboard access.
|
|
23
|
-
interface AccessibleTooltipProps extends Omit<
|
|
24
|
-
XyChartTooltipProps< DataPointDate >,
|
|
25
|
-
'renderTooltip'
|
|
26
|
-
> {
|
|
27
|
-
renderTooltip?: ( params: RenderTooltipParams< DataPointDate > ) => ReactNode;
|
|
28
|
-
selectedIndex?: number | undefined;
|
|
29
|
-
tooltipRef?: ( element: HTMLDivElement | null ) => void;
|
|
30
|
-
keyboardFocusedClassName?: string;
|
|
31
|
-
/**
|
|
32
|
-
* Flattened tooltip data prepared by parent component
|
|
33
|
-
* Each index corresponds to one tooltip to show
|
|
34
|
-
*/
|
|
35
|
-
tooltipData?: FlattenedTooltipData[];
|
|
36
|
-
/**
|
|
37
|
-
* For line charts: series data to show all series at selected data point
|
|
38
|
-
* When provided, shows all series instead of individual tooltips
|
|
39
|
-
*/
|
|
40
|
-
series?: SeriesData[];
|
|
41
|
-
/**
|
|
42
|
-
* Whether to combine tooltip information from multiple series into a single tooltip. This is useful for line charts.
|
|
43
|
-
* Or to show individual tooltips for each series. This is useful for bar charts.
|
|
44
|
-
*/
|
|
45
|
-
mode?: 'individual' | 'group';
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export const AccessibleTooltip: React.FC< AccessibleTooltipProps > = ( {
|
|
49
|
-
renderTooltip,
|
|
50
|
-
selectedIndex,
|
|
51
|
-
tooltipRef,
|
|
52
|
-
keyboardFocusedClassName,
|
|
53
|
-
series = [],
|
|
54
|
-
mode = 'group',
|
|
55
|
-
verticalCrosshairStyle,
|
|
56
|
-
horizontalCrosshairStyle,
|
|
57
|
-
...props
|
|
58
|
-
} ) => {
|
|
59
|
-
const tooltipContext = useContext( TooltipContext );
|
|
60
|
-
const scopeElement = useChartScopeElement();
|
|
61
|
-
|
|
62
|
-
// The stroke is read at the scope element, which a consumer can set outside the chart's own ancestors; see TOKENS.md#the-svg-bridge.
|
|
63
|
-
const crosshairStroke = useMemo( () => {
|
|
64
|
-
const stroke = resolveCssVariable( CATALOG_POINTERS.grid, scopeElement );
|
|
65
|
-
|
|
66
|
-
// Passing `stroke: undefined` would erase the crosshair: it overrides visx's own value, and SVG's initial `stroke` is `none`.
|
|
67
|
-
return stroke ? { stroke } : undefined;
|
|
68
|
-
}, [ scopeElement ] );
|
|
69
|
-
|
|
70
|
-
const standaloneScopeClass = useStandaloneScopeClass();
|
|
71
|
-
|
|
72
|
-
const tooltipData = useMemo( () => {
|
|
73
|
-
if ( mode !== 'individual' ) return [];
|
|
74
|
-
if ( series.length === 0 ) return [];
|
|
75
|
-
|
|
76
|
-
const maxDataPoints = Math.max( ...series.map( s => s.data.length ) );
|
|
77
|
-
const flattened: Array< {
|
|
78
|
-
datum: DataPointDate;
|
|
79
|
-
seriesLabel: string;
|
|
80
|
-
seriesIndex: number;
|
|
81
|
-
dataPointIndex: number;
|
|
82
|
-
} > = [];
|
|
83
|
-
|
|
84
|
-
// Pattern: [series1[0], series2[0], series3[0], series1[1], series2[1], series3[1], ...]
|
|
85
|
-
for ( let dataPointIndex = 0; dataPointIndex < maxDataPoints; dataPointIndex++ ) {
|
|
86
|
-
for ( let seriesIndex = 0; seriesIndex < series.length; seriesIndex++ ) {
|
|
87
|
-
const seriesData = series[ seriesIndex ];
|
|
88
|
-
if ( dataPointIndex < seriesData.data.length ) {
|
|
89
|
-
flattened.push( {
|
|
90
|
-
datum: seriesData.data[ dataPointIndex ] as DataPointDate,
|
|
91
|
-
seriesLabel: seriesData.label,
|
|
92
|
-
seriesIndex,
|
|
93
|
-
dataPointIndex,
|
|
94
|
-
} );
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return flattened;
|
|
100
|
-
}, [ series, mode ] );
|
|
101
|
-
|
|
102
|
-
// Tracks whether this effect opened a tooltip, so it only closes its own.
|
|
103
|
-
const hasKeyboardSelection = useRef( false );
|
|
104
|
-
|
|
105
|
-
// visx's own `hideTooltip` is debounced by 400ms, long enough for a stale datum to be
|
|
106
|
-
// repainted against a changed series list. This is the undebounced close it wraps.
|
|
107
|
-
const closeTooltipNow = useCallback( () => {
|
|
108
|
-
tooltipContext?.updateTooltip( {
|
|
109
|
-
tooltipOpen: false,
|
|
110
|
-
tooltipLeft: undefined,
|
|
111
|
-
tooltipTop: undefined,
|
|
112
|
-
tooltipData: undefined,
|
|
113
|
-
} );
|
|
114
|
-
// Don't include tooltipContext in the dependency array to avoid loop.
|
|
115
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
116
|
-
}, [] );
|
|
117
|
-
|
|
118
|
-
const chartId = useContext( ChartInstanceContext )?.chartId;
|
|
119
|
-
const { getHiddenSeries } = useGlobalChartsContext();
|
|
120
|
-
const hiddenSeriesKey = chartId
|
|
121
|
-
? JSON.stringify( [ ...getHiddenSeries( chartId ) ].sort() )
|
|
122
|
-
: undefined;
|
|
123
|
-
const lastHiddenSeriesKey = useRef( hiddenSeriesKey );
|
|
124
|
-
|
|
125
|
-
// Hiding a series re-points every tooltip index at a different series, so a tooltip opened by
|
|
126
|
-
// the pointer would keep showing a datum that is no longer on the chart. Nothing else closes
|
|
127
|
-
// it: the pointer has not moved, so visx never fires the leave that would.
|
|
128
|
-
useEffect( () => {
|
|
129
|
-
const changed = lastHiddenSeriesKey.current !== hiddenSeriesKey;
|
|
130
|
-
lastHiddenSeriesKey.current = hiddenSeriesKey;
|
|
131
|
-
|
|
132
|
-
// A keyboard selection is reconciled by `useKeyboardNavigation` instead, which either
|
|
133
|
-
// re-shows the tooltip at a valid index or clears the selection for the branch below.
|
|
134
|
-
if ( changed && selectedIndex === undefined ) {
|
|
135
|
-
closeTooltipNow();
|
|
136
|
-
}
|
|
137
|
-
}, [ hiddenSeriesKey, selectedIndex, closeTooltipNow ] );
|
|
138
|
-
|
|
139
|
-
// Handle tooltip highlighting for keyboard navigation
|
|
140
|
-
useEffect( () => {
|
|
141
|
-
if ( selectedIndex === undefined ) {
|
|
142
|
-
// visx debounces the hide and cancels only the most recently scheduled one,
|
|
143
|
-
// so hiding on every run leaves earlier hides pending. One of those lands
|
|
144
|
-
// mid-navigation and closes the tooltip the user is reading.
|
|
145
|
-
if ( hasKeyboardSelection.current ) {
|
|
146
|
-
hasKeyboardSelection.current = false;
|
|
147
|
-
closeTooltipNow();
|
|
148
|
-
}
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
hasKeyboardSelection.current = true;
|
|
153
|
-
|
|
154
|
-
if ( mode === 'group' ) {
|
|
155
|
-
// Show all series at the selected data point index in single tooltip.
|
|
156
|
-
series.forEach( ( s, index ) => {
|
|
157
|
-
if ( selectedIndex < s.data.length ) {
|
|
158
|
-
const datum = s.data[ selectedIndex ];
|
|
159
|
-
|
|
160
|
-
tooltipContext?.showTooltip( {
|
|
161
|
-
datum,
|
|
162
|
-
key: s.label,
|
|
163
|
-
index,
|
|
164
|
-
} );
|
|
165
|
-
}
|
|
166
|
-
} );
|
|
167
|
-
} else if ( mode === 'individual' ) {
|
|
168
|
-
// Show individual tooltips for each datapoint from each series.
|
|
169
|
-
if ( selectedIndex < tooltipData.length ) {
|
|
170
|
-
const tooltipItem = tooltipData[ selectedIndex ];
|
|
171
|
-
|
|
172
|
-
tooltipContext?.showTooltip( {
|
|
173
|
-
datum: tooltipItem.datum,
|
|
174
|
-
key: tooltipItem.seriesLabel,
|
|
175
|
-
index: tooltipItem.dataPointIndex,
|
|
176
|
-
} );
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Don't include tooltipContext in the dependency array to avoid loop.
|
|
181
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
182
|
-
}, [ selectedIndex, tooltipData, series ] );
|
|
183
|
-
|
|
184
|
-
// Create a focusable renderTooltip that includes accessibility features
|
|
185
|
-
const focusableRenderTooltip = useMemo( () => {
|
|
186
|
-
if ( ! renderTooltip ) return undefined;
|
|
187
|
-
|
|
188
|
-
return ( params: RenderTooltipParams< DataPointDate > ) => {
|
|
189
|
-
const tooltipContent = renderTooltip( params );
|
|
190
|
-
|
|
191
|
-
if ( selectedIndex !== undefined ) {
|
|
192
|
-
return (
|
|
193
|
-
<div
|
|
194
|
-
ref={ tooltipRef }
|
|
195
|
-
tabIndex={ -1 }
|
|
196
|
-
role="tooltip"
|
|
197
|
-
aria-atomic="true"
|
|
198
|
-
className={ clsx( standaloneScopeClass, keyboardFocusedClassName ) }
|
|
199
|
-
data-testid={ `chart-tooltip-${ selectedIndex }` }
|
|
200
|
-
key={ `chart-tooltip-${ selectedIndex }` }
|
|
201
|
-
>
|
|
202
|
-
{ tooltipContent }
|
|
203
|
-
</div>
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return (
|
|
208
|
-
<div className={ standaloneScopeClass } role="tooltip" aria-live="polite">
|
|
209
|
-
{ tooltipContent }
|
|
210
|
-
</div>
|
|
211
|
-
);
|
|
212
|
-
};
|
|
213
|
-
}, [ renderTooltip, selectedIndex, tooltipRef, keyboardFocusedClassName, standaloneScopeClass ] );
|
|
214
|
-
|
|
215
|
-
return (
|
|
216
|
-
<XyChartTooltip
|
|
217
|
-
{ ...props }
|
|
218
|
-
verticalCrosshairStyle={ { ...crosshairStroke, ...verticalCrosshairStyle } }
|
|
219
|
-
horizontalCrosshairStyle={ { ...crosshairStroke, ...horizontalCrosshairStyle } }
|
|
220
|
-
renderTooltip={ focusableRenderTooltip }
|
|
221
|
-
/>
|
|
222
|
-
);
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
// Keyboard navigation hook for charts
|
|
226
|
-
interface UseKeyboardNavigationProps {
|
|
227
|
-
selectedIndex: number | undefined;
|
|
228
|
-
setSelectedIndex: ( index: number | undefined ) => void;
|
|
229
|
-
isNavigating: boolean;
|
|
230
|
-
setIsNavigating: ( navigating: boolean ) => void;
|
|
231
|
-
chartRef: React.RefObject< HTMLDivElement >;
|
|
232
|
-
/**
|
|
233
|
-
* Total number of navigation points (length of tooltip data array)
|
|
234
|
-
*/
|
|
235
|
-
totalPoints: number;
|
|
236
|
-
/**
|
|
237
|
-
* Called with the selected index on Enter or Space, so a chart can treat the
|
|
238
|
-
* keyboard selection the way it treats a click.
|
|
239
|
-
*/
|
|
240
|
-
onActivate?: ( index: number ) => void;
|
|
241
|
-
preventTooltipScroll?: boolean;
|
|
242
|
-
/**
|
|
243
|
-
* Identity of the series the navigation indexes run over, for charts whose index space depends
|
|
244
|
-
* on which series are visible. Swapping one hidden series for another of the same length leaves
|
|
245
|
-
* `totalPoints` unchanged, so the count alone cannot detect it.
|
|
246
|
-
*/
|
|
247
|
-
visibleSeriesKey?: string;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export const useKeyboardNavigation = ( {
|
|
251
|
-
selectedIndex,
|
|
252
|
-
setSelectedIndex,
|
|
253
|
-
isNavigating,
|
|
254
|
-
setIsNavigating,
|
|
255
|
-
chartRef,
|
|
256
|
-
totalPoints,
|
|
257
|
-
onActivate,
|
|
258
|
-
preventTooltipScroll = false,
|
|
259
|
-
visibleSeriesKey,
|
|
260
|
-
}: UseKeyboardNavigationProps ) => {
|
|
261
|
-
// `chartRef` sits inside the focusable grid, so the grid is what focus returns to and is measured against.
|
|
262
|
-
const getChartRoot = useCallback(
|
|
263
|
-
(): HTMLElement | null =>
|
|
264
|
-
chartRef.current?.closest< HTMLElement >( '[role="grid"]' ) ?? chartRef.current,
|
|
265
|
-
[ chartRef ]
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
const focusWithoutScrollIfNeeded = useCallback(
|
|
269
|
-
( element: HTMLElement | null | undefined ) => {
|
|
270
|
-
if ( preventTooltipScroll ) {
|
|
271
|
-
element?.focus( { preventScroll: true } );
|
|
272
|
-
} else {
|
|
273
|
-
element?.focus();
|
|
274
|
-
}
|
|
275
|
-
},
|
|
276
|
-
[ preventTooltipScroll ]
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
// Focus the tooltip as soon as it is rendered
|
|
280
|
-
const tooltipRef = useCallback(
|
|
281
|
-
( element: HTMLDivElement | null ) => {
|
|
282
|
-
if ( element && selectedIndex !== undefined ) {
|
|
283
|
-
focusWithoutScrollIfNeeded( element );
|
|
284
|
-
}
|
|
285
|
-
},
|
|
286
|
-
[ focusWithoutScrollIfNeeded, selectedIndex ]
|
|
287
|
-
);
|
|
288
|
-
|
|
289
|
-
const previousTotalPoints = useRef( totalPoints );
|
|
290
|
-
const previousVisibleSeriesKey = useRef( visibleSeriesKey );
|
|
291
|
-
|
|
292
|
-
// Hiding or showing a series moves every index onto a different bar. The count alone misses a
|
|
293
|
-
// swap that keeps it the same, such as hiding one series while revealing another of equal length.
|
|
294
|
-
// Clear rather than clamp once focus has left the chart: a new tooltip would pull focus back.
|
|
295
|
-
useEffect( () => {
|
|
296
|
-
const countChanged = previousTotalPoints.current !== totalPoints;
|
|
297
|
-
const seriesChanged = previousVisibleSeriesKey.current !== visibleSeriesKey;
|
|
298
|
-
previousTotalPoints.current = totalPoints;
|
|
299
|
-
previousVisibleSeriesKey.current = visibleSeriesKey;
|
|
300
|
-
|
|
301
|
-
if ( ( ! countChanged && ! seriesChanged ) || selectedIndex === undefined ) {
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const { activeElement } = document;
|
|
306
|
-
const focusIsInChart = activeElement !== null && !! getChartRoot()?.contains( activeElement );
|
|
307
|
-
|
|
308
|
-
// A swap that leaves the count alone gives the same index a different series, so unlike a
|
|
309
|
-
// count change there is no shorter range to clamp into.
|
|
310
|
-
if ( totalPoints === 0 || ! focusIsInChart || ! countChanged ) {
|
|
311
|
-
setSelectedIndex( undefined );
|
|
312
|
-
setIsNavigating( false );
|
|
313
|
-
// Clearing unmounts the focused tooltip, which would drop focus to the body.
|
|
314
|
-
if ( focusIsInChart ) {
|
|
315
|
-
focusWithoutScrollIfNeeded( getChartRoot() );
|
|
316
|
-
}
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
setSelectedIndex( Math.min( selectedIndex, totalPoints - 1 ) );
|
|
321
|
-
}, [
|
|
322
|
-
selectedIndex,
|
|
323
|
-
totalPoints,
|
|
324
|
-
visibleSeriesKey,
|
|
325
|
-
setSelectedIndex,
|
|
326
|
-
setIsNavigating,
|
|
327
|
-
getChartRoot,
|
|
328
|
-
focusWithoutScrollIfNeeded,
|
|
329
|
-
] );
|
|
330
|
-
|
|
331
|
-
// Returning focus from the tooltip must not restore the selection Escape just cleared.
|
|
332
|
-
const onChartFocus = useCallback(
|
|
333
|
-
( event: React.FocusEvent< HTMLDivElement > ) => {
|
|
334
|
-
if ( event.currentTarget.contains( event.relatedTarget ) ) {
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
if ( ! isNavigating && selectedIndex !== undefined ) {
|
|
338
|
-
setSelectedIndex( 0 );
|
|
339
|
-
}
|
|
340
|
-
},
|
|
341
|
-
[ isNavigating, selectedIndex, setSelectedIndex ]
|
|
342
|
-
);
|
|
343
|
-
|
|
344
|
-
// The point the pointer took over a keyboard selection at, where the next arrow key continues from.
|
|
345
|
-
const pointerIndex = useRef< number | undefined >( undefined );
|
|
346
|
-
|
|
347
|
-
// The pointer is the latest input, so it ends a keyboard selection instead of competing with it.
|
|
348
|
-
const onChartPointerMove = useCallback(
|
|
349
|
-
( index: number ) => {
|
|
350
|
-
if ( selectedIndex === undefined ) {
|
|
351
|
-
if ( pointerIndex.current !== undefined ) {
|
|
352
|
-
pointerIndex.current = index;
|
|
353
|
-
}
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
const root = getChartRoot();
|
|
357
|
-
const activeElement = root?.ownerDocument.activeElement;
|
|
358
|
-
// Focus elsewhere on the page stays there; a hover must not pull it back into the chart.
|
|
359
|
-
if ( activeElement && root.contains( activeElement ) ) {
|
|
360
|
-
pointerIndex.current = index;
|
|
361
|
-
focusWithoutScrollIfNeeded( root );
|
|
362
|
-
}
|
|
363
|
-
setSelectedIndex( undefined );
|
|
364
|
-
setIsNavigating( false );
|
|
365
|
-
},
|
|
366
|
-
[ selectedIndex, setSelectedIndex, setIsNavigating, getChartRoot, focusWithoutScrollIfNeeded ]
|
|
367
|
-
);
|
|
368
|
-
|
|
369
|
-
// On each blur of chart, keyboard navigation should restart from first tooltip
|
|
370
|
-
const onChartBlur = useCallback(
|
|
371
|
-
( event: React.FocusEvent< HTMLDivElement > ) => {
|
|
372
|
-
setIsNavigating( false );
|
|
373
|
-
if ( ! event.currentTarget.contains( event.relatedTarget ) ) {
|
|
374
|
-
pointerIndex.current = undefined;
|
|
375
|
-
}
|
|
376
|
-
},
|
|
377
|
-
[ setIsNavigating ]
|
|
378
|
-
);
|
|
379
|
-
|
|
380
|
-
const onChartKeyDown = useCallback(
|
|
381
|
-
( event: React.KeyboardEvent< HTMLDivElement > ) => {
|
|
382
|
-
if ( event.key === 'Tab' || event.key === 'Escape' ) {
|
|
383
|
-
// Consuming Escape with nothing open would swallow the first Escape of a surrounding Modal.
|
|
384
|
-
if ( event.key === 'Escape' && selectedIndex !== undefined ) {
|
|
385
|
-
event.preventDefault();
|
|
386
|
-
}
|
|
387
|
-
focusWithoutScrollIfNeeded( getChartRoot() );
|
|
388
|
-
setSelectedIndex( undefined );
|
|
389
|
-
setIsNavigating( false );
|
|
390
|
-
pointerIndex.current = undefined;
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if ( totalPoints === 0 ) return;
|
|
395
|
-
|
|
396
|
-
const currentSelectedIndex = selectedIndex ?? pointerIndex.current ?? -1;
|
|
397
|
-
if ( event.key === 'ArrowRight' || event.key === 'ArrowLeft' ) {
|
|
398
|
-
pointerIndex.current = undefined;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// WAI-ARIA grid: arrows stop at the first and last cell.
|
|
402
|
-
if ( event.key === 'ArrowRight' ) {
|
|
403
|
-
event.preventDefault();
|
|
404
|
-
setIsNavigating( true );
|
|
405
|
-
setSelectedIndex( Math.min( currentSelectedIndex + 1, totalPoints - 1 ) );
|
|
406
|
-
} else if ( event.key === 'ArrowLeft' ) {
|
|
407
|
-
event.preventDefault();
|
|
408
|
-
setIsNavigating( true );
|
|
409
|
-
setSelectedIndex( Math.max( currentSelectedIndex - 1, 0 ) );
|
|
410
|
-
} else if ( ( event.key === 'Enter' || event.key === ' ' ) && selectedIndex !== undefined ) {
|
|
411
|
-
event.preventDefault();
|
|
412
|
-
onActivate?.( selectedIndex );
|
|
413
|
-
}
|
|
414
|
-
},
|
|
415
|
-
[
|
|
416
|
-
totalPoints,
|
|
417
|
-
selectedIndex,
|
|
418
|
-
setSelectedIndex,
|
|
419
|
-
setIsNavigating,
|
|
420
|
-
getChartRoot,
|
|
421
|
-
focusWithoutScrollIfNeeded,
|
|
422
|
-
onActivate,
|
|
423
|
-
]
|
|
424
|
-
);
|
|
425
|
-
|
|
426
|
-
return {
|
|
427
|
-
tooltipRef,
|
|
428
|
-
onChartFocus,
|
|
429
|
-
onChartBlur,
|
|
430
|
-
onChartKeyDown,
|
|
431
|
-
onChartPointerMove,
|
|
432
|
-
};
|
|
433
|
-
};
|