@automattic/charts 4.8.0 → 4.10.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 +28 -0
- package/dist/index.cjs +766 -277
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +72 -8
- package/dist/index.d.cts +54 -15
- package/dist/index.d.ts +54 -15
- package/dist/index.js +770 -281
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.tsx +7 -3
- package/src/charts/area-chart/test/area-chart.test.tsx +15 -14
- package/src/charts/area-chart/test/tooltip-resolution.test.tsx +2 -2
- package/src/charts/bar-chart/bar-chart.tsx +12 -4
- package/src/charts/bar-chart/test/bar-chart.test.tsx +74 -63
- package/src/charts/bar-list-chart/bar-list-chart.tsx +3 -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 +23 -7
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +22 -0
- package/src/charts/conversion-funnel-chart/types.ts +1 -1
- package/src/charts/geo-chart/geo-chart.tsx +17 -2
- package/src/charts/geo-chart/test/geo-chart.test.tsx +9 -4
- package/src/charts/geo-chart/types.ts +1 -1
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +10 -4
- package/src/charts/heatmap-chart/heatmap-chart.tsx +144 -37
- 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-legend.tsx +14 -2
- package/src/charts/heatmap-chart/private/index.ts +4 -2
- 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 +132 -7
- package/src/charts/heatmap-chart/types.ts +2 -2
- package/src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts +2 -2
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +52 -4
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +60 -9
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +80 -1
- package/src/charts/leaderboard-chart/types.ts +7 -1
- package/src/charts/line-chart/line-chart.tsx +8 -2
- 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 +37 -35
- package/src/charts/line-chart/test/tooltip-resolution.test.tsx +2 -2
- package/src/charts/line-chart/types.ts +3 -3
- package/src/charts/pie-chart/pie-chart.module.scss +3 -0
- package/src/charts/pie-chart/pie-chart.tsx +144 -139
- package/src/charts/pie-chart/test/pie-chart.test.tsx +167 -0
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +3 -0
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +117 -120
- package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +54 -0
- 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 +49 -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/x-zoom/test/x-zoom.test.tsx +3 -2
- package/src/charts/test/accessible-name.test.tsx +181 -0
- package/src/components/legend/hooks/test/use-chart-legend-items.test.tsx +63 -3
- package/src/components/legend/hooks/use-chart-legend-items.ts +63 -1
- package/src/components/legend/private/base-legend.tsx +27 -15
- package/src/components/legend/test/legend.test.tsx +42 -0
- package/src/components/legend/types.ts +2 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +20 -19
- package/src/components/tooltip/xy-chart-tooltip.tsx +3 -3
- package/src/hooks/test/use-xychart-theme.test.tsx +22 -0
- package/src/hooks/use-xychart-theme.ts +33 -2
- package/src/index.ts +1 -0
- package/src/types.ts +37 -3
- package/src/utils/resolve-font-size.ts +9 -10
- package/src/utils/test/resolve-font-size.test.ts +11 -2
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { Group } from '@visx/group';
|
|
2
|
-
import {
|
|
3
|
-
import { useTooltip } from '@visx/tooltip';
|
|
2
|
+
import { arc, pie } from '@visx/shape';
|
|
4
3
|
import { color as d3Color } from '@visx/vendor/d3-color';
|
|
4
|
+
import { __ } from '@wordpress/i18n';
|
|
5
5
|
import clsx from 'clsx';
|
|
6
6
|
import isEqual from 'fast-deep-equal';
|
|
7
|
-
import {
|
|
7
|
+
import { useContext, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
8
8
|
import { Legend, useChartLegendItems } from '../../components/legend';
|
|
9
|
-
import { BoundedTooltip } from '../../components/tooltip/private/bounded-tooltip';
|
|
10
9
|
import { LabelValueContent } from '../../components/tooltip/private/label-value-content';
|
|
11
10
|
import {
|
|
12
11
|
useDataWithPercentages,
|
|
@@ -22,7 +21,6 @@ import {
|
|
|
22
21
|
GlobalChartsContext,
|
|
23
22
|
} from '../../providers';
|
|
24
23
|
import { CATALOG_POINTERS } from '../../providers/chart-context/private/catalog-pointers';
|
|
25
|
-
import { useStandaloneScopeClass } from '../../providers/chart-scope';
|
|
26
24
|
import { attachSubComponents, createCssVariableResolver, resolveFontSize } from '../../utils';
|
|
27
25
|
import { getStringWidth } from '../../visx/text';
|
|
28
26
|
import { Center } from '../private/center';
|
|
@@ -30,6 +28,12 @@ import { ChartSVG, ChartHTML, useChartChildren } from '../private/chart-composit
|
|
|
30
28
|
import { ChartInstanceContext } from '../private/chart-instance-context';
|
|
31
29
|
import { ChartLayout } from '../private/chart-layout';
|
|
32
30
|
import { pickLabelTextColor, resolveLabelRoles } from '../private/label-text-color';
|
|
31
|
+
import {
|
|
32
|
+
orderArcsForNavigation,
|
|
33
|
+
PieSelectionOutput,
|
|
34
|
+
SelectedSegmentRing,
|
|
35
|
+
usePieKeyboardNavigation,
|
|
36
|
+
} from '../private/pie-keyboard-navigation';
|
|
33
37
|
import { RadialWipeAnimation } from '../private/radial-wipe-animation/';
|
|
34
38
|
import { getAllHiddenMessage, SvgEmptyState } from '../private/svg-empty-state';
|
|
35
39
|
import { withResponsive, ResponsiveConfig } from '../private/with-responsive';
|
|
@@ -43,7 +47,11 @@ import type {
|
|
|
43
47
|
} from '../../types';
|
|
44
48
|
import type { ChartComponentWithComposition } from '../private/chart-composition';
|
|
45
49
|
import type { LabelRoles, LabelTextColor } from '../private/label-text-color';
|
|
46
|
-
import type {
|
|
50
|
+
import type { PieProvidedProps } from '@visx/shape';
|
|
51
|
+
import type { JSX, ReactNode, FC } from 'react';
|
|
52
|
+
|
|
53
|
+
type PieDatum = DataPointPercentageCalculated & { index: number };
|
|
54
|
+
type PieArcDatum = PieProvidedProps< PieDatum >[ 'arcs' ][ number ];
|
|
47
55
|
|
|
48
56
|
/**
|
|
49
57
|
* Parameters passed to the renderTooltip function for pie charts.
|
|
@@ -201,6 +209,7 @@ const PieChartInternal = ( {
|
|
|
201
209
|
tooltipOffsetX = 0,
|
|
202
210
|
tooltipOffsetY = -15,
|
|
203
211
|
renderTooltip = renderDefaultPieTooltip,
|
|
212
|
+
ariaLabel,
|
|
204
213
|
gap = 'md',
|
|
205
214
|
}: PieChartProps ) => {
|
|
206
215
|
const legendInteractive = legend.interactive ?? false;
|
|
@@ -208,12 +217,6 @@ const PieChartInternal = ( {
|
|
|
208
217
|
|
|
209
218
|
const providerTheme = useGlobalChartsTheme();
|
|
210
219
|
const chartId = useChartId( providedChartId );
|
|
211
|
-
const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } =
|
|
212
|
-
useTooltip< DataPointPercentageCalculated >();
|
|
213
|
-
const standaloneScopeClass = useStandaloneScopeClass();
|
|
214
|
-
|
|
215
|
-
// The tooltip renders inside this element, so pointer coordinates are taken relative to it.
|
|
216
|
-
const containerRef = useRef< HTMLDivElement >( null );
|
|
217
220
|
|
|
218
221
|
// The element the chart's own `className` lands on, so an override set there reaches this
|
|
219
222
|
// decision the same way it reaches CSS.
|
|
@@ -221,13 +224,6 @@ const PieChartInternal = ( {
|
|
|
221
224
|
// Null until read, and while a label plate is set: text on the plate keeps the inverse role.
|
|
222
225
|
const [ labelRoles, setLabelRoles ] = useState< LabelRoles | null >( null );
|
|
223
226
|
|
|
224
|
-
const onMouseLeave = useCallback( () => {
|
|
225
|
-
if ( ! withTooltips ) {
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
hideTooltip();
|
|
229
|
-
}, [ withTooltips, hideTooltip ] );
|
|
230
|
-
|
|
231
227
|
const { getElementStyles, isSeriesVisible, isColorPaletteResolved } = useGlobalChartsContext();
|
|
232
228
|
const { isValid, message } = validateData( data );
|
|
233
229
|
|
|
@@ -292,14 +288,7 @@ const PieChartInternal = ( {
|
|
|
292
288
|
} );
|
|
293
289
|
|
|
294
290
|
const prefersReducedMotion = usePrefersReducedMotion();
|
|
295
|
-
|
|
296
|
-
if ( ! isValid ) {
|
|
297
|
-
return (
|
|
298
|
-
<div className={ clsx( 'pie-chart', styles[ 'pie-chart' ], className ) }>
|
|
299
|
-
<div className={ styles[ 'error-message' ] }>{ message }</div>
|
|
300
|
-
</div>
|
|
301
|
-
);
|
|
302
|
-
}
|
|
291
|
+
const wipeMask = animation && ! prefersReducedMotion ? `url(#radial-wipe-${ chartId })` : null;
|
|
303
292
|
|
|
304
293
|
// Calculate the angle between each (use original data length for consistent spacing)
|
|
305
294
|
const padAngle = gapScale * ( ( 2 * Math.PI ) / data.length );
|
|
@@ -321,6 +310,32 @@ const PieChartInternal = ( {
|
|
|
321
310
|
},
|
|
322
311
|
};
|
|
323
312
|
|
|
313
|
+
const arcs = pie< PieDatum >( { value: accessors.value, padAngle } )( dataWithIndex );
|
|
314
|
+
const navigationArcs = orderArcsForNavigation( arcs );
|
|
315
|
+
|
|
316
|
+
const {
|
|
317
|
+
chartRef,
|
|
318
|
+
svgRef,
|
|
319
|
+
selectedArc,
|
|
320
|
+
getSegmentHandlers,
|
|
321
|
+
getKeyboardTooltipAnchor,
|
|
322
|
+
selectionOutputProps,
|
|
323
|
+
chartProps,
|
|
324
|
+
} = usePieKeyboardNavigation( {
|
|
325
|
+
navigationArcs,
|
|
326
|
+
withTooltips,
|
|
327
|
+
tooltipOffsetX,
|
|
328
|
+
tooltipOffsetY,
|
|
329
|
+
} );
|
|
330
|
+
|
|
331
|
+
if ( ! isValid ) {
|
|
332
|
+
return (
|
|
333
|
+
<div className={ clsx( 'pie-chart', styles[ 'pie-chart' ], className ) }>
|
|
334
|
+
<div className={ styles[ 'error-message' ] }>{ message }</div>
|
|
335
|
+
</div>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
324
339
|
const legendElement = showLegend && (
|
|
325
340
|
<Legend
|
|
326
341
|
orientation={ legend.orientation ?? 'horizontal' }
|
|
@@ -383,9 +398,72 @@ const PieChartInternal = ( {
|
|
|
383
398
|
? Math.min( cornerScale * outerRadius, maxCornerRadius )
|
|
384
399
|
: 0;
|
|
385
400
|
|
|
401
|
+
const path = arc< PieArcDatum >( { innerRadius, outerRadius, cornerRadius } );
|
|
402
|
+
|
|
403
|
+
const svgLabelSmall = providerTheme.svgLabelSmall;
|
|
404
|
+
const fontSize = resolveFontSize( svgLabelSmall?.fontSize ) ?? 12;
|
|
405
|
+
|
|
406
|
+
const renderLabel = ( arcDatum: PieArcDatum, index: number ) => {
|
|
407
|
+
if ( arcDatum.endAngle - arcDatum.startAngle < 0.25 ) {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const [ centroidX, centroidY ] = path.centroid( arcDatum );
|
|
412
|
+
const estimatedTextWidth = getStringWidth( arcDatum.data.label, {
|
|
413
|
+
fontSize,
|
|
414
|
+
fontFamily: svgLabelSmall?.fontFamily,
|
|
415
|
+
fontWeight: svgLabelSmall?.fontWeight,
|
|
416
|
+
} );
|
|
417
|
+
const labelPadding = 6;
|
|
418
|
+
const backgroundWidth = estimatedTextWidth + labelPadding * 2;
|
|
419
|
+
const backgroundHeight = fontSize + labelPadding * 2;
|
|
420
|
+
|
|
421
|
+
return (
|
|
422
|
+
<g key={ `label-${ index }` }>
|
|
423
|
+
<rect
|
|
424
|
+
className={ styles[ 'pie-chart__label-plate' ] }
|
|
425
|
+
x={ centroidX - backgroundWidth / 2 }
|
|
426
|
+
y={ centroidY - backgroundHeight / 2 }
|
|
427
|
+
width={ backgroundWidth }
|
|
428
|
+
height={ backgroundHeight }
|
|
429
|
+
rx={ 4 }
|
|
430
|
+
ry={ 4 }
|
|
431
|
+
pointerEvents="none"
|
|
432
|
+
/>
|
|
433
|
+
<text
|
|
434
|
+
className={ clsx(
|
|
435
|
+
styles[ 'pie-chart__label-text' ],
|
|
436
|
+
LABEL_TEXT_MODIFIER[
|
|
437
|
+
pickLabelTextColor(
|
|
438
|
+
accessors.fill( arcDatum.data ),
|
|
439
|
+
labelRoles,
|
|
440
|
+
'label-inverse'
|
|
441
|
+
)
|
|
442
|
+
]
|
|
443
|
+
) }
|
|
444
|
+
data-testid="pie-label"
|
|
445
|
+
x={ centroidX }
|
|
446
|
+
y={ centroidY }
|
|
447
|
+
dy=".33em"
|
|
448
|
+
fontSize={ fontSize }
|
|
449
|
+
textAnchor="middle"
|
|
450
|
+
pointerEvents="none"
|
|
451
|
+
>
|
|
452
|
+
{ arcDatum.data.label }
|
|
453
|
+
</text>
|
|
454
|
+
</g>
|
|
455
|
+
);
|
|
456
|
+
};
|
|
457
|
+
|
|
386
458
|
return (
|
|
387
|
-
<Center
|
|
459
|
+
<Center
|
|
460
|
+
ref={ chartRef }
|
|
461
|
+
className={ styles[ 'pie-chart__plot' ] }
|
|
462
|
+
{ ...chartProps }
|
|
463
|
+
aria-label={ ariaLabel ?? __( 'Pie chart', 'jetpack-charts' ) }
|
|
464
|
+
>
|
|
388
465
|
<svg
|
|
466
|
+
ref={ svgRef }
|
|
389
467
|
viewBox={ `0 0 ${ width } ${ height }` }
|
|
390
468
|
preserveAspectRatio="xMidYMid meet"
|
|
391
469
|
width={ width }
|
|
@@ -399,125 +477,52 @@ const PieChartInternal = ( {
|
|
|
399
477
|
/>
|
|
400
478
|
</defs>
|
|
401
479
|
|
|
402
|
-
<Group
|
|
403
|
-
top={ centerY }
|
|
404
|
-
left={ centerX }
|
|
405
|
-
mask={
|
|
406
|
-
animation && ! prefersReducedMotion ? `url(#radial-wipe-${ chartId })` : null
|
|
407
|
-
}
|
|
408
|
-
>
|
|
480
|
+
<Group top={ centerY } left={ centerX } mask={ wipeMask }>
|
|
409
481
|
{ allSegmentsHidden ? (
|
|
410
482
|
<SvgEmptyState x={ 0 } y={ 0 } width={ width } height={ height }>
|
|
411
483
|
{ getAllHiddenMessage( legendInteractive, 'segments' ) }
|
|
412
484
|
</SvgEmptyState>
|
|
413
485
|
) : (
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const bounds = containerRef.current?.getBoundingClientRect();
|
|
432
|
-
if ( ! bounds ) {
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
showTooltip( {
|
|
437
|
-
tooltipData: arc.data,
|
|
438
|
-
tooltipLeft: event.clientX - bounds.left + tooltipOffsetX,
|
|
439
|
-
tooltipTop: event.clientY - bounds.top + tooltipOffsetY,
|
|
440
|
-
} );
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
const fill = accessors.fill( arc.data );
|
|
444
|
-
const pathProps: SVGProps< SVGPathElement > & {
|
|
445
|
-
'data-testid'?: string;
|
|
446
|
-
} = {
|
|
447
|
-
d: pie.path( arc ) || '',
|
|
448
|
-
fill,
|
|
449
|
-
'data-testid': 'pie-segment',
|
|
450
|
-
};
|
|
451
|
-
|
|
452
|
-
const groupProps: SVGProps< SVGGElement > = {};
|
|
453
|
-
if ( withTooltips ) {
|
|
454
|
-
groupProps.onMouseMove = handleMouseMove;
|
|
455
|
-
groupProps.onMouseLeave = onMouseLeave;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const svgLabelSmall = providerTheme.svgLabelSmall;
|
|
459
|
-
const fontSize = resolveFontSize( svgLabelSmall?.fontSize ) ?? 12;
|
|
460
|
-
const estimatedTextWidth = getStringWidth( arc.data.label, {
|
|
461
|
-
fontSize,
|
|
462
|
-
fontFamily: svgLabelSmall?.fontFamily,
|
|
463
|
-
fontWeight: svgLabelSmall?.fontWeight,
|
|
464
|
-
} );
|
|
465
|
-
const labelPadding = 6;
|
|
466
|
-
const backgroundWidth = estimatedTextWidth + labelPadding * 2;
|
|
467
|
-
const backgroundHeight = fontSize + labelPadding * 2;
|
|
468
|
-
|
|
469
|
-
return (
|
|
470
|
-
<g key={ `arc-${ index }` } { ...groupProps }>
|
|
471
|
-
<path { ...pathProps } />
|
|
472
|
-
{ showLabels && hasSpaceForLabel && (
|
|
473
|
-
<g>
|
|
474
|
-
<rect
|
|
475
|
-
className={ styles[ 'pie-chart__label-plate' ] }
|
|
476
|
-
x={ centroidX - backgroundWidth / 2 }
|
|
477
|
-
y={ centroidY - backgroundHeight / 2 }
|
|
478
|
-
width={ backgroundWidth }
|
|
479
|
-
height={ backgroundHeight }
|
|
480
|
-
rx={ 4 }
|
|
481
|
-
ry={ 4 }
|
|
482
|
-
pointerEvents="none"
|
|
483
|
-
/>
|
|
484
|
-
<text
|
|
485
|
-
className={ clsx(
|
|
486
|
-
styles[ 'pie-chart__label-text' ],
|
|
487
|
-
LABEL_TEXT_MODIFIER[
|
|
488
|
-
pickLabelTextColor( fill, labelRoles, 'label-inverse' )
|
|
489
|
-
]
|
|
490
|
-
) }
|
|
491
|
-
data-testid="pie-label"
|
|
492
|
-
x={ centroidX }
|
|
493
|
-
y={ centroidY }
|
|
494
|
-
dy=".33em"
|
|
495
|
-
fontSize={ fontSize }
|
|
496
|
-
textAnchor="middle"
|
|
497
|
-
pointerEvents="none"
|
|
498
|
-
>
|
|
499
|
-
{ arc.data.label }
|
|
500
|
-
</text>
|
|
501
|
-
</g>
|
|
502
|
-
) }
|
|
503
|
-
</g>
|
|
504
|
-
);
|
|
505
|
-
} );
|
|
506
|
-
} }
|
|
507
|
-
</Pie>
|
|
486
|
+
arcs.map( ( arcDatum, index ) => (
|
|
487
|
+
<g
|
|
488
|
+
key={ `arc-${ index }` }
|
|
489
|
+
{ ...getSegmentHandlers(
|
|
490
|
+
arcDatum.data,
|
|
491
|
+
navigationArcs.indexOf( arcDatum )
|
|
492
|
+
) }
|
|
493
|
+
>
|
|
494
|
+
<path
|
|
495
|
+
d={ path( arcDatum ) || '' }
|
|
496
|
+
fill={ accessors.fill( arcDatum.data ) }
|
|
497
|
+
data-testid="pie-segment"
|
|
498
|
+
/>
|
|
499
|
+
</g>
|
|
500
|
+
) )
|
|
508
501
|
) }
|
|
509
|
-
|
|
510
|
-
{ /* Render SVG children (like Group, Text) inside the SVG */ }
|
|
511
|
-
{ ! allSegmentsHidden && svgChildren }
|
|
512
502
|
</Group>
|
|
503
|
+
{ /* Unmasked so the entry wipe cannot hide the focus indicator. */ }
|
|
504
|
+
{ selectedArc && (
|
|
505
|
+
<Group top={ centerY } left={ centerX }>
|
|
506
|
+
<SelectedSegmentRing chartId={ chartId } d={ path( selectedArc ) || '' } />
|
|
507
|
+
</Group>
|
|
508
|
+
) }
|
|
509
|
+
{ ! allSegmentsHidden && (
|
|
510
|
+
<Group top={ centerY } left={ centerX } mask={ wipeMask }>
|
|
511
|
+
{ showLabels && arcs.map( renderLabel ) }
|
|
512
|
+
|
|
513
|
+
{ /* Render SVG children (like Group, Text) inside the SVG */ }
|
|
514
|
+
{ svgChildren }
|
|
515
|
+
</Group>
|
|
516
|
+
) }
|
|
513
517
|
</svg>
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
518
|
+
<PieSelectionOutput
|
|
519
|
+
{ ...selectionOutputProps }
|
|
520
|
+
keyboardTooltipAnchor={
|
|
521
|
+
selectedArc &&
|
|
522
|
+
getKeyboardTooltipAnchor( centerX, centerY, path.centroid( selectedArc ) )
|
|
523
|
+
}
|
|
524
|
+
renderTooltip={ renderTooltip }
|
|
525
|
+
/>
|
|
521
526
|
</Center>
|
|
522
527
|
);
|
|
523
528
|
} }
|
|
@@ -773,6 +773,117 @@ describe( 'PieChart', () => {
|
|
|
773
773
|
expect( screen.queryByText( /Click legend items to show data/i ) ).not.toBeInTheDocument();
|
|
774
774
|
} );
|
|
775
775
|
} );
|
|
776
|
+
|
|
777
|
+
describe( 'Keyboard navigation', () => {
|
|
778
|
+
const data = [
|
|
779
|
+
{ label: 'MacOS', value: 30000, valueDisplay: '30K' },
|
|
780
|
+
{ label: 'Linux', value: 22000, valueDisplay: '22K' },
|
|
781
|
+
];
|
|
782
|
+
|
|
783
|
+
const expectRingOn = ( segment: HTMLElement ) =>
|
|
784
|
+
expect( screen.getByTestId( 'pie-selected-ring' ) ).toHaveAttribute(
|
|
785
|
+
'd',
|
|
786
|
+
segment.getAttribute( 'd' )
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
test( 'arrow keys move the focused tooltip and highlight across segments', async () => {
|
|
790
|
+
const user = userEvent.setup();
|
|
791
|
+
render( <PieChart data={ data } withTooltips /> );
|
|
792
|
+
const [ macos, linux ] = screen.getAllByTestId( 'pie-segment' );
|
|
793
|
+
expect( screen.queryByTestId( 'pie-selected-ring' ) ).not.toBeInTheDocument();
|
|
794
|
+
expect( screen.getByRole( 'application', { name: 'Pie chart' } ) ).toHaveAttribute(
|
|
795
|
+
'tabindex',
|
|
796
|
+
'0'
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
await user.tab();
|
|
800
|
+
await user.keyboard( '{ArrowRight}' );
|
|
801
|
+
await expect( screen.findByTestId( 'chart-tooltip-0' ) ).resolves.toHaveTextContent(
|
|
802
|
+
'MacOS'
|
|
803
|
+
);
|
|
804
|
+
expect( screen.getByTestId( 'chart-tooltip-0' ) ).toHaveFocus();
|
|
805
|
+
expectRingOn( macos );
|
|
806
|
+
|
|
807
|
+
await user.keyboard( '{ArrowRight}' );
|
|
808
|
+
await expect( screen.findByTestId( 'chart-tooltip-1' ) ).resolves.toHaveTextContent(
|
|
809
|
+
'Linux'
|
|
810
|
+
);
|
|
811
|
+
expectRingOn( linux );
|
|
812
|
+
} );
|
|
813
|
+
|
|
814
|
+
test( 'announces and highlights a segment without a tooltip when withTooltips is false', async () => {
|
|
815
|
+
const user = userEvent.setup();
|
|
816
|
+
render( <PieChart data={ data } /> );
|
|
817
|
+
const status = screen.getByRole( 'status' );
|
|
818
|
+
expect( status ).toBeEmptyDOMElement();
|
|
819
|
+
|
|
820
|
+
await user.tab();
|
|
821
|
+
await user.keyboard( '{ArrowRight}' );
|
|
822
|
+
expectRingOn( screen.getAllByTestId( 'pie-segment' )[ 0 ] );
|
|
823
|
+
expect( screen.queryByRole( 'tooltip' ) ).not.toBeInTheDocument();
|
|
824
|
+
expect( status ).toHaveTextContent( 'MacOS: 30K' );
|
|
825
|
+
} );
|
|
826
|
+
|
|
827
|
+
test( 'leaves the announcement to the focused tooltip when withTooltips is true', async () => {
|
|
828
|
+
const user = userEvent.setup();
|
|
829
|
+
render( <PieChart data={ data } withTooltips /> );
|
|
830
|
+
|
|
831
|
+
await user.tab();
|
|
832
|
+
await user.keyboard( '{ArrowRight}' );
|
|
833
|
+
await expect( screen.findByTestId( 'chart-tooltip-0' ) ).resolves.toBeInTheDocument();
|
|
834
|
+
expect( screen.getByRole( 'status' ) ).toBeEmptyDOMElement();
|
|
835
|
+
} );
|
|
836
|
+
|
|
837
|
+
test.each( [
|
|
838
|
+
[ true, [ 'Linux: 22K' ] ],
|
|
839
|
+
[ false, [] ],
|
|
840
|
+
] )(
|
|
841
|
+
'hovering a segment ends the keyboard selection (withTooltips %p)',
|
|
842
|
+
async ( withTooltips, tooltips ) => {
|
|
843
|
+
const user = userEvent.setup();
|
|
844
|
+
render( <PieChart data={ data } withTooltips={ withTooltips } /> );
|
|
845
|
+
|
|
846
|
+
await user.tab();
|
|
847
|
+
await user.keyboard( '{ArrowRight}' );
|
|
848
|
+
expect( screen.getByTestId( 'pie-selected-ring' ) ).toBeInTheDocument();
|
|
849
|
+
|
|
850
|
+
await user.hover( screen.getAllByTestId( 'pie-segment' )[ 1 ] );
|
|
851
|
+
|
|
852
|
+
expect( screen.queryByTestId( 'pie-selected-ring' ) ).not.toBeInTheDocument();
|
|
853
|
+
expect( screen.queryByTestId( 'chart-tooltip-0' ) ).not.toBeInTheDocument();
|
|
854
|
+
expect( screen.queryAllByRole( 'tooltip' ).map( tooltip => tooltip.textContent ) ).toEqual(
|
|
855
|
+
tooltips
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
test( 'keeps focus in the chart when tooltips are turned off under a keyboard selection', async () => {
|
|
861
|
+
const user = userEvent.setup();
|
|
862
|
+
const { rerender } = render( <PieChart data={ data } withTooltips /> );
|
|
863
|
+
|
|
864
|
+
await user.tab();
|
|
865
|
+
await user.keyboard( '{ArrowRight}{ArrowRight}' );
|
|
866
|
+
await expect( screen.findByTestId( 'chart-tooltip-1' ) ).resolves.toHaveFocus();
|
|
867
|
+
|
|
868
|
+
rerender( <PieChart data={ data } withTooltips={ false } /> );
|
|
869
|
+
|
|
870
|
+
expect( screen.getByRole( 'application' ) ).toHaveFocus();
|
|
871
|
+
expectRingOn( screen.getAllByTestId( 'pie-segment' )[ 1 ] );
|
|
872
|
+
} );
|
|
873
|
+
|
|
874
|
+
test( 'keeps focus in the chart when an update removes the selected segment', async () => {
|
|
875
|
+
const user = userEvent.setup();
|
|
876
|
+
const { rerender } = render( <PieChart data={ data } withTooltips /> );
|
|
877
|
+
|
|
878
|
+
await user.tab();
|
|
879
|
+
await user.keyboard( '{ArrowRight}{ArrowRight}' );
|
|
880
|
+
await expect( screen.findByTestId( 'chart-tooltip-1' ) ).resolves.toHaveFocus();
|
|
881
|
+
|
|
882
|
+
rerender( <PieChart data={ data.slice( 0, 1 ) } withTooltips /> );
|
|
883
|
+
|
|
884
|
+
await expect( screen.findByTestId( 'chart-tooltip-0' ) ).resolves.toHaveFocus();
|
|
885
|
+
} );
|
|
886
|
+
} );
|
|
776
887
|
} );
|
|
777
888
|
|
|
778
889
|
// Chart container at (100, 50); the tooltip box measures 120x40. Everything
|
|
@@ -829,4 +940,60 @@ describe( 'PieChart tooltip position', () => {
|
|
|
829
940
|
transform: 'translate(60px, 65px)',
|
|
830
941
|
} );
|
|
831
942
|
} );
|
|
943
|
+
|
|
944
|
+
test( 'moves the keyboard tooltip with the segment when the chart resizes', async () => {
|
|
945
|
+
const rects = mockRects();
|
|
946
|
+
const rectOf = rects.getMockImplementation();
|
|
947
|
+
let svgShift = 0;
|
|
948
|
+
// The plot is centered in the 400x300 container, so its offset follows its committed size.
|
|
949
|
+
rects.mockImplementation( function ( this: Element ) {
|
|
950
|
+
const rect = rectOf.call( this );
|
|
951
|
+
if ( ! ( this instanceof SVGSVGElement && this.hasAttribute( 'viewBox' ) ) ) {
|
|
952
|
+
return rect;
|
|
953
|
+
}
|
|
954
|
+
const size = Number( this.getAttribute( 'width' ) );
|
|
955
|
+
return {
|
|
956
|
+
...rect,
|
|
957
|
+
left: 100 + ( 400 - size ) / 2 + svgShift,
|
|
958
|
+
top: 50 + ( 300 - size ) / 2 + svgShift,
|
|
959
|
+
};
|
|
960
|
+
} );
|
|
961
|
+
const user = userEvent.setup();
|
|
962
|
+
const data = [
|
|
963
|
+
{ label: 'A', value: 60, valueDisplay: '60' },
|
|
964
|
+
{ label: 'B', value: 40, valueDisplay: '40' },
|
|
965
|
+
];
|
|
966
|
+
const renderAtSize = ( size: number ) => (
|
|
967
|
+
<GlobalChartsProvider>
|
|
968
|
+
<PieChart data={ data } width={ 400 } height={ 300 } size={ size } withTooltips />
|
|
969
|
+
</GlobalChartsProvider>
|
|
970
|
+
);
|
|
971
|
+
const selectFirstSegment = async () => {
|
|
972
|
+
await user.tab();
|
|
973
|
+
await user.keyboard( '{ArrowRight}' );
|
|
974
|
+
await expect( screen.findByTestId( 'chart-tooltip-0' ) ).resolves.toHaveFocus();
|
|
975
|
+
return screen.getByTestId( 'bounded-tooltip' ).style.transform;
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
const translation = ( transform: string ) => transform.match( /-?[\d.]+/g )?.map( Number );
|
|
979
|
+
|
|
980
|
+
const view = render( renderAtSize( 100 ) );
|
|
981
|
+
const expected = await selectFirstSegment();
|
|
982
|
+
view.unmount();
|
|
983
|
+
|
|
984
|
+
svgShift = 7;
|
|
985
|
+
const { unmount } = render( renderAtSize( 100 ) );
|
|
986
|
+
expect( translation( await selectFirstSegment() ) ).toEqual(
|
|
987
|
+
translation( expected )?.map( value => value + 7 )
|
|
988
|
+
);
|
|
989
|
+
unmount();
|
|
990
|
+
svgShift = 0;
|
|
991
|
+
|
|
992
|
+
const { rerender } = render( renderAtSize( 200 ) );
|
|
993
|
+
await expect( selectFirstSegment() ).resolves.not.toBe( expected );
|
|
994
|
+
rerender( renderAtSize( 100 ) );
|
|
995
|
+
|
|
996
|
+
await expect( screen.findByTestId( 'chart-tooltip-0' ) ).resolves.toHaveFocus();
|
|
997
|
+
expect( screen.getByTestId( 'bounded-tooltip' ) ).toHaveStyle( { transform: expected } );
|
|
998
|
+
} );
|
|
832
999
|
} );
|