@automattic/charts 0.57.0 → 0.59.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 +36 -2
- package/README.md +7 -54
- package/dist/index.cjs +9607 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +32 -49
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +1612 -33
- package/dist/index.d.ts +1612 -33
- package/dist/index.js +9640 -54
- package/dist/index.js.map +1 -1
- package/package.json +9 -126
- package/src/charts/bar-chart/bar-chart.module.scss +0 -5
- package/src/charts/bar-chart/bar-chart.tsx +142 -149
- package/src/charts/bar-chart/test/bar-chart.test.tsx +48 -31
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +54 -74
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +4 -5
- package/src/charts/leaderboard-chart/types.ts +1 -11
- package/src/charts/line-chart/line-chart.module.scss +0 -5
- package/src/charts/line-chart/line-chart.tsx +202 -193
- package/src/charts/line-chart/private/line-chart-annotations-overlay.tsx +1 -2
- package/src/charts/line-chart/test/line-chart.test.tsx +49 -27
- package/src/charts/line-chart/types.ts +0 -1
- package/src/charts/pie-chart/pie-chart.module.scss +2 -10
- package/src/charts/pie-chart/pie-chart.tsx +212 -212
- package/src/charts/pie-chart/test/composition-api.test.tsx +44 -3
- package/src/charts/pie-chart/test/pie-chart.test.tsx +51 -44
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +2 -8
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +166 -168
- package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +58 -30
- package/src/charts/private/chart-composition/index.ts +2 -0
- package/src/charts/private/chart-composition/render-legend-slot.ts +22 -0
- package/src/charts/private/chart-composition/test/render-legend-slot.test.tsx +60 -0
- package/src/charts/private/chart-composition/test/use-chart-children.test.tsx +91 -0
- package/src/charts/private/chart-composition/use-chart-children.ts +34 -2
- package/src/charts/private/chart-layout/chart-layout.module.scss +7 -0
- package/src/charts/private/chart-layout/chart-layout.tsx +106 -0
- package/src/charts/private/chart-layout/index.ts +2 -0
- package/src/charts/private/chart-layout/test/chart-layout.test.tsx +167 -0
- package/src/charts/private/single-chart-context/single-chart-context.tsx +2 -2
- package/src/charts/private/svg-empty-state/index.ts +1 -0
- package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +7 -0
- package/src/charts/private/svg-empty-state/svg-empty-state.tsx +40 -0
- package/src/components/legend/hooks/test/use-chart-legend-items.test.tsx +12 -8
- package/src/components/legend/hooks/use-chart-legend-items.ts +12 -13
- package/src/components/legend/index.ts +1 -8
- package/src/components/legend/legend.tsx +33 -8
- package/src/components/legend/private/base-legend.module.scss +19 -37
- package/src/components/legend/private/base-legend.tsx +0 -2
- package/src/components/legend/test/legend.test.tsx +93 -1
- package/src/components/legend/types.ts +7 -34
- package/src/hooks/index.ts +1 -1
- package/src/hooks/use-data-with-percentages.ts +24 -0
- package/src/hooks/use-interactive-legend-data.ts +18 -15
- package/src/index.ts +66 -9
- package/src/providers/chart-context/global-charts-provider.tsx +7 -1
- package/src/providers/chart-context/hooks/use-chart-registration.ts +2 -1
- package/src/providers/chart-context/types.ts +2 -2
- package/src/types.ts +110 -45
- package/src/utils/date-parsing.ts +10 -1
- package/src/utils/test/date-parsing.test.ts +12 -0
- package/src/utils/test/resolve-css-var.test.ts +4 -2
- package/tsup.config.ts +1 -1
- package/dist/base-tooltip-DOq93wjU.d.cts +0 -38
- package/dist/base-tooltip-DOq93wjU.d.ts +0 -38
- package/dist/charts/bar-chart/index.cjs +0 -15
- package/dist/charts/bar-chart/index.cjs.map +0 -1
- package/dist/charts/bar-chart/index.css +0 -153
- package/dist/charts/bar-chart/index.css.map +0 -1
- package/dist/charts/bar-chart/index.d.cts +0 -37
- package/dist/charts/bar-chart/index.d.ts +0 -37
- package/dist/charts/bar-chart/index.js +0 -15
- package/dist/charts/bar-chart/index.js.map +0 -1
- package/dist/charts/bar-list-chart/index.cjs +0 -16
- package/dist/charts/bar-list-chart/index.cjs.map +0 -1
- package/dist/charts/bar-list-chart/index.css +0 -153
- package/dist/charts/bar-list-chart/index.css.map +0 -1
- package/dist/charts/bar-list-chart/index.d.cts +0 -92
- package/dist/charts/bar-list-chart/index.d.ts +0 -92
- package/dist/charts/bar-list-chart/index.js +0 -16
- package/dist/charts/bar-list-chart/index.js.map +0 -1
- package/dist/charts/conversion-funnel-chart/index.cjs +0 -11
- package/dist/charts/conversion-funnel-chart/index.cjs.map +0 -1
- package/dist/charts/conversion-funnel-chart/index.css +0 -251
- package/dist/charts/conversion-funnel-chart/index.css.map +0 -1
- package/dist/charts/conversion-funnel-chart/index.d.cts +0 -97
- package/dist/charts/conversion-funnel-chart/index.d.ts +0 -97
- package/dist/charts/conversion-funnel-chart/index.js +0 -11
- package/dist/charts/conversion-funnel-chart/index.js.map +0 -1
- package/dist/charts/geo-chart/index.cjs +0 -13
- package/dist/charts/geo-chart/index.cjs.map +0 -1
- package/dist/charts/geo-chart/index.css +0 -117
- package/dist/charts/geo-chart/index.css.map +0 -1
- package/dist/charts/geo-chart/index.d.cts +0 -67
- package/dist/charts/geo-chart/index.d.ts +0 -67
- package/dist/charts/geo-chart/index.js +0 -13
- package/dist/charts/geo-chart/index.js.map +0 -1
- package/dist/charts/leaderboard-chart/index.cjs +0 -20
- package/dist/charts/leaderboard-chart/index.cjs.map +0 -1
- package/dist/charts/leaderboard-chart/index.css +0 -172
- package/dist/charts/leaderboard-chart/index.css.map +0 -1
- package/dist/charts/leaderboard-chart/index.d.cts +0 -46
- package/dist/charts/leaderboard-chart/index.d.ts +0 -46
- package/dist/charts/leaderboard-chart/index.js +0 -20
- package/dist/charts/leaderboard-chart/index.js.map +0 -1
- package/dist/charts/line-chart/index.cjs +0 -15
- package/dist/charts/line-chart/index.cjs.map +0 -1
- package/dist/charts/line-chart/index.css +0 -225
- package/dist/charts/line-chart/index.css.map +0 -1
- package/dist/charts/line-chart/index.d.cts +0 -99
- package/dist/charts/line-chart/index.d.ts +0 -99
- package/dist/charts/line-chart/index.js +0 -15
- package/dist/charts/line-chart/index.js.map +0 -1
- package/dist/charts/pie-chart/index.cjs +0 -18
- package/dist/charts/pie-chart/index.cjs.map +0 -1
- package/dist/charts/pie-chart/index.css +0 -143
- package/dist/charts/pie-chart/index.css.map +0 -1
- package/dist/charts/pie-chart/index.d.cts +0 -97
- package/dist/charts/pie-chart/index.d.ts +0 -97
- package/dist/charts/pie-chart/index.js +0 -18
- package/dist/charts/pie-chart/index.js.map +0 -1
- package/dist/charts/pie-semi-circle-chart/index.cjs +0 -17
- package/dist/charts/pie-semi-circle-chart/index.cjs.map +0 -1
- package/dist/charts/pie-semi-circle-chart/index.css +0 -144
- package/dist/charts/pie-semi-circle-chart/index.css.map +0 -1
- package/dist/charts/pie-semi-circle-chart/index.d.cts +0 -94
- package/dist/charts/pie-semi-circle-chart/index.d.ts +0 -94
- package/dist/charts/pie-semi-circle-chart/index.js +0 -17
- package/dist/charts/pie-semi-circle-chart/index.js.map +0 -1
- package/dist/charts/sparkline/index.cjs +0 -16
- package/dist/charts/sparkline/index.cjs.map +0 -1
- package/dist/charts/sparkline/index.css +0 -242
- package/dist/charts/sparkline/index.css.map +0 -1
- package/dist/charts/sparkline/index.d.cts +0 -113
- package/dist/charts/sparkline/index.d.ts +0 -113
- package/dist/charts/sparkline/index.js +0 -16
- package/dist/charts/sparkline/index.js.map +0 -1
- package/dist/chunk-2A34OA5O.cjs +0 -51
- package/dist/chunk-2A34OA5O.cjs.map +0 -1
- package/dist/chunk-2NCY7R4G.js +0 -3897
- package/dist/chunk-2NCY7R4G.js.map +0 -1
- package/dist/chunk-32DH6JDF.js +0 -1263
- package/dist/chunk-32DH6JDF.js.map +0 -1
- package/dist/chunk-4OPFE4RM.js +0 -614
- package/dist/chunk-4OPFE4RM.js.map +0 -1
- package/dist/chunk-6CCZL2JJ.js +0 -63
- package/dist/chunk-6CCZL2JJ.js.map +0 -1
- package/dist/chunk-77OKCVQN.cjs +0 -421
- package/dist/chunk-77OKCVQN.cjs.map +0 -1
- package/dist/chunk-7FQX4ALL.cjs +0 -219
- package/dist/chunk-7FQX4ALL.cjs.map +0 -1
- package/dist/chunk-ASLARV7L.cjs +0 -81
- package/dist/chunk-ASLARV7L.cjs.map +0 -1
- package/dist/chunk-BCX5THDQ.js +0 -403
- package/dist/chunk-BCX5THDQ.js.map +0 -1
- package/dist/chunk-BPYKWMI7.js +0 -194
- package/dist/chunk-BPYKWMI7.js.map +0 -1
- package/dist/chunk-CZGYJKG6.js +0 -421
- package/dist/chunk-CZGYJKG6.js.map +0 -1
- package/dist/chunk-D2UH4CFE.cjs +0 -120
- package/dist/chunk-D2UH4CFE.cjs.map +0 -1
- package/dist/chunk-DAU3HNEG.js +0 -344
- package/dist/chunk-DAU3HNEG.js.map +0 -1
- package/dist/chunk-H2V4JMSA.js +0 -219
- package/dist/chunk-H2V4JMSA.js.map +0 -1
- package/dist/chunk-I2276W3I.cjs +0 -66
- package/dist/chunk-I2276W3I.cjs.map +0 -1
- package/dist/chunk-I35UYJJR.cjs +0 -468
- package/dist/chunk-I35UYJJR.cjs.map +0 -1
- package/dist/chunk-IU4DYUAV.js +0 -120
- package/dist/chunk-IU4DYUAV.js.map +0 -1
- package/dist/chunk-KXRWNFQJ.js +0 -51
- package/dist/chunk-KXRWNFQJ.js.map +0 -1
- package/dist/chunk-OP6PHB2U.js +0 -81
- package/dist/chunk-OP6PHB2U.js.map +0 -1
- package/dist/chunk-PXLEMUGJ.js +0 -165
- package/dist/chunk-PXLEMUGJ.js.map +0 -1
- package/dist/chunk-RCY6XLGU.cjs +0 -63
- package/dist/chunk-RCY6XLGU.cjs.map +0 -1
- package/dist/chunk-RHHVEJHJ.cjs +0 -1263
- package/dist/chunk-RHHVEJHJ.cjs.map +0 -1
- package/dist/chunk-TO3OQBXG.cjs +0 -165
- package/dist/chunk-TO3OQBXG.cjs.map +0 -1
- package/dist/chunk-V36ERY7Y.js +0 -375
- package/dist/chunk-V36ERY7Y.js.map +0 -1
- package/dist/chunk-VJM5XCB4.cjs +0 -614
- package/dist/chunk-VJM5XCB4.cjs.map +0 -1
- package/dist/chunk-VTS3PNMS.cjs +0 -344
- package/dist/chunk-VTS3PNMS.cjs.map +0 -1
- package/dist/chunk-WLODYNLB.js +0 -1067
- package/dist/chunk-WLODYNLB.js.map +0 -1
- package/dist/chunk-XKRJL2QT.cjs +0 -375
- package/dist/chunk-XKRJL2QT.cjs.map +0 -1
- package/dist/chunk-XWYZIFZW.js +0 -66
- package/dist/chunk-XWYZIFZW.js.map +0 -1
- package/dist/chunk-Y3NNQMAX.cjs +0 -194
- package/dist/chunk-Y3NNQMAX.cjs.map +0 -1
- package/dist/chunk-YE2T52VZ.cjs +0 -1067
- package/dist/chunk-YE2T52VZ.cjs.map +0 -1
- package/dist/chunk-Z26M4V2M.js +0 -468
- package/dist/chunk-Z26M4V2M.js.map +0 -1
- package/dist/chunk-Z45KX47P.cjs +0 -3897
- package/dist/chunk-Z45KX47P.cjs.map +0 -1
- package/dist/chunk-ZH4F5RMG.cjs +0 -403
- package/dist/chunk-ZH4F5RMG.cjs.map +0 -1
- package/dist/components/legend/index.cjs +0 -11
- package/dist/components/legend/index.cjs.map +0 -1
- package/dist/components/legend/index.css +0 -103
- package/dist/components/legend/index.css.map +0 -1
- package/dist/components/legend/index.d.cts +0 -37
- package/dist/components/legend/index.d.ts +0 -37
- package/dist/components/legend/index.js +0 -11
- package/dist/components/legend/index.js.map +0 -1
- package/dist/components/tooltip/index.cjs +0 -12
- package/dist/components/tooltip/index.cjs.map +0 -1
- package/dist/components/tooltip/index.css +0 -13
- package/dist/components/tooltip/index.css.map +0 -1
- package/dist/components/tooltip/index.d.cts +0 -71
- package/dist/components/tooltip/index.d.ts +0 -71
- package/dist/components/tooltip/index.js +0 -12
- package/dist/components/tooltip/index.js.map +0 -1
- package/dist/components/trend-indicator/index.cjs +0 -8
- package/dist/components/trend-indicator/index.cjs.map +0 -1
- package/dist/components/trend-indicator/index.css +0 -27
- package/dist/components/trend-indicator/index.css.map +0 -1
- package/dist/components/trend-indicator/index.d.cts +0 -44
- package/dist/components/trend-indicator/index.d.ts +0 -44
- package/dist/components/trend-indicator/index.js +0 -8
- package/dist/components/trend-indicator/index.js.map +0 -1
- package/dist/format-metric-value-MXm5DtQ_.d.cts +0 -24
- package/dist/format-metric-value-MXm5DtQ_.d.ts +0 -24
- package/dist/hooks/index.cjs +0 -31
- package/dist/hooks/index.cjs.map +0 -1
- package/dist/hooks/index.css +0 -103
- package/dist/hooks/index.css.map +0 -1
- package/dist/hooks/index.d.cts +0 -272
- package/dist/hooks/index.d.ts +0 -272
- package/dist/hooks/index.js +0 -31
- package/dist/hooks/index.js.map +0 -1
- package/dist/leaderboard-chart-BKYYXcg2.d.ts +0 -83
- package/dist/leaderboard-chart-DR7CGb0L.d.cts +0 -83
- package/dist/legend-C2grwnWk.d.cts +0 -9
- package/dist/legend-Cj0xM5dU.d.ts +0 -9
- package/dist/providers/index.cjs +0 -21
- package/dist/providers/index.cjs.map +0 -1
- package/dist/providers/index.css +0 -103
- package/dist/providers/index.css.map +0 -1
- package/dist/providers/index.d.cts +0 -28
- package/dist/providers/index.d.ts +0 -28
- package/dist/providers/index.js +0 -21
- package/dist/providers/index.js.map +0 -1
- package/dist/themes-BmVGrYnF.d.ts +0 -67
- package/dist/themes-CyjKm-P_.d.cts +0 -67
- package/dist/types-CuUEszrM.d.ts +0 -19
- package/dist/types-DZordNiO.d.cts +0 -505
- package/dist/types-DZordNiO.d.ts +0 -505
- package/dist/types-I67mddpr.d.cts +0 -78
- package/dist/types-I67mddpr.d.ts +0 -78
- package/dist/types-KtOPPzPX.d.cts +0 -19
- package/dist/utils/index.cjs +0 -44
- package/dist/utils/index.cjs.map +0 -1
- package/dist/utils/index.d.cts +0 -226
- package/dist/utils/index.d.ts +0 -226
- package/dist/utils/index.js +0 -44
- package/dist/utils/index.js.map +0 -1
- package/dist/with-responsive-CNfhzAUu.d.cts +0 -18
- package/dist/with-responsive-CNfhzAUu.d.ts +0 -18
- package/src/hooks/use-has-legend-child.ts +0 -22
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createElement, Fragment } from 'react';
|
|
2
|
+
import type { LegendChild } from './use-chart-children';
|
|
3
|
+
import type { LegendPosition } from '../../../types';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Renders legend children filtered by position slot.
|
|
8
|
+
*
|
|
9
|
+
* @param {LegendChild[]} legendChildren - The legend children to filter and render
|
|
10
|
+
* @param {LegendPosition} position - The position slot to render
|
|
11
|
+
* @return {ReactNode[]} Array of legend elements for the given position
|
|
12
|
+
*/
|
|
13
|
+
export function renderLegendSlot(
|
|
14
|
+
legendChildren: LegendChild[],
|
|
15
|
+
position: LegendPosition
|
|
16
|
+
): ReactNode[] {
|
|
17
|
+
return legendChildren
|
|
18
|
+
.filter( l => l.position === position )
|
|
19
|
+
.map( ( l, i ) =>
|
|
20
|
+
createElement( Fragment, { key: `legend-${ position }-${ i }` }, l.element )
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createElement } from 'react';
|
|
2
|
+
import { renderLegendSlot } from '../render-legend-slot';
|
|
3
|
+
import type { LegendChild } from '../use-chart-children';
|
|
4
|
+
|
|
5
|
+
const makeLegend = ( position: 'top' | 'bottom', label: string ): LegendChild => ( {
|
|
6
|
+
element: createElement( 'div', null, label ),
|
|
7
|
+
position,
|
|
8
|
+
} );
|
|
9
|
+
|
|
10
|
+
describe( 'renderLegendSlot', () => {
|
|
11
|
+
it( 'should return an empty array when given no children', () => {
|
|
12
|
+
expect( renderLegendSlot( [], 'top' ) ).toHaveLength( 0 );
|
|
13
|
+
} );
|
|
14
|
+
|
|
15
|
+
it( 'should return an empty array when no children match the position', () => {
|
|
16
|
+
const children = [ makeLegend( 'bottom', 'Legend 1' ) ];
|
|
17
|
+
|
|
18
|
+
expect( renderLegendSlot( children, 'top' ) ).toHaveLength( 0 );
|
|
19
|
+
} );
|
|
20
|
+
|
|
21
|
+
it( 'should return a single matching child', () => {
|
|
22
|
+
const children = [ makeLegend( 'top', 'Legend 1' ) ];
|
|
23
|
+
const view = renderLegendSlot( children, 'top' );
|
|
24
|
+
|
|
25
|
+
expect( view ).toHaveLength( 1 );
|
|
26
|
+
expect( view[ 0 ] ).toHaveProperty( 'key', 'legend-top-0' );
|
|
27
|
+
} );
|
|
28
|
+
|
|
29
|
+
it( 'should return multiple matching children in order', () => {
|
|
30
|
+
const children = [ makeLegend( 'bottom', 'Legend 1' ), makeLegend( 'bottom', 'Legend 2' ) ];
|
|
31
|
+
const view = renderLegendSlot( children, 'bottom' );
|
|
32
|
+
|
|
33
|
+
expect( view ).toHaveLength( 2 );
|
|
34
|
+
expect( view[ 0 ] ).toHaveProperty( 'key', 'legend-bottom-0' );
|
|
35
|
+
expect( view[ 1 ] ).toHaveProperty( 'key', 'legend-bottom-1' );
|
|
36
|
+
} );
|
|
37
|
+
|
|
38
|
+
it( 'should filter out children with a different position', () => {
|
|
39
|
+
const children = [
|
|
40
|
+
makeLegend( 'top', 'Top Legend' ),
|
|
41
|
+
makeLegend( 'bottom', 'Bottom Legend' ),
|
|
42
|
+
makeLegend( 'top', 'Another Top Legend' ),
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
expect( renderLegendSlot( children, 'top' ) ).toHaveLength( 2 );
|
|
46
|
+
expect( renderLegendSlot( children, 'bottom' ) ).toHaveLength( 1 );
|
|
47
|
+
} );
|
|
48
|
+
|
|
49
|
+
it( 'should produce distinct keys for top and bottom slots', () => {
|
|
50
|
+
const children = [ makeLegend( 'top', 'Top' ), makeLegend( 'bottom', 'Bottom' ) ];
|
|
51
|
+
const [ topElement, bottomElement ] = [
|
|
52
|
+
renderLegendSlot( children, 'top' )[ 0 ],
|
|
53
|
+
renderLegendSlot( children, 'bottom' )[ 0 ],
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
expect( topElement ).toHaveProperty( 'key', 'legend-top-0' );
|
|
57
|
+
expect( bottomElement ).toHaveProperty( 'key', 'legend-bottom-0' );
|
|
58
|
+
expect( topElement ).not.toEqual( bottomElement );
|
|
59
|
+
} );
|
|
60
|
+
} );
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { renderHook } from '@testing-library/react';
|
|
2
2
|
import { Group } from '@visx/group';
|
|
3
|
+
import { Legend } from '../../../../components/legend';
|
|
3
4
|
import { ChartSVG, ChartHTML } from '../index';
|
|
4
5
|
import { useChartChildren } from '../use-chart-children';
|
|
5
6
|
|
|
@@ -22,6 +23,7 @@ describe( 'useChartChildren', () => {
|
|
|
22
23
|
|
|
23
24
|
expect( result.current.svgChildren ).toHaveLength( 1 );
|
|
24
25
|
expect( result.current.htmlChildren ).toHaveLength( 0 );
|
|
26
|
+
expect( result.current.legendChildren ).toHaveLength( 0 );
|
|
25
27
|
expect( result.current.otherChildren ).toHaveLength( 0 );
|
|
26
28
|
} );
|
|
27
29
|
|
|
@@ -58,6 +60,7 @@ describe( 'useChartChildren', () => {
|
|
|
58
60
|
|
|
59
61
|
expect( result.current.svgChildren ).toHaveLength( 1 );
|
|
60
62
|
expect( result.current.htmlChildren ).toHaveLength( 1 );
|
|
63
|
+
expect( result.current.legendChildren ).toHaveLength( 0 );
|
|
61
64
|
expect( result.current.otherChildren ).toHaveLength( 0 );
|
|
62
65
|
} );
|
|
63
66
|
|
|
@@ -112,6 +115,7 @@ describe( 'useChartChildren', () => {
|
|
|
112
115
|
|
|
113
116
|
expect( result.current.svgChildren ).toHaveLength( 0 );
|
|
114
117
|
expect( result.current.htmlChildren ).toHaveLength( 0 );
|
|
118
|
+
expect( result.current.legendChildren ).toHaveLength( 0 );
|
|
115
119
|
expect( result.current.otherChildren ).toHaveLength( 0 );
|
|
116
120
|
} );
|
|
117
121
|
|
|
@@ -128,4 +132,91 @@ describe( 'useChartChildren', () => {
|
|
|
128
132
|
|
|
129
133
|
expect( result.current.svgChildren ).toHaveLength( 3 );
|
|
130
134
|
} );
|
|
135
|
+
|
|
136
|
+
it( 'should extract Legend children to legendChildren with default position bottom', () => {
|
|
137
|
+
const children = <Legend />;
|
|
138
|
+
|
|
139
|
+
const { result } = renderHook( () => useChartChildren( children, 'TestChart' ) );
|
|
140
|
+
|
|
141
|
+
expect( result.current.legendChildren ).toHaveLength( 1 );
|
|
142
|
+
expect( result.current.legendChildren[ 0 ].position ).toBe( 'bottom' );
|
|
143
|
+
expect( result.current.otherChildren ).toHaveLength( 0 );
|
|
144
|
+
} );
|
|
145
|
+
|
|
146
|
+
it( 'should extract Legend children with position top', () => {
|
|
147
|
+
const children = <Legend position="top" />;
|
|
148
|
+
|
|
149
|
+
const { result } = renderHook( () => useChartChildren( children, 'TestChart' ) );
|
|
150
|
+
|
|
151
|
+
expect( result.current.legendChildren ).toHaveLength( 1 );
|
|
152
|
+
expect( result.current.legendChildren[ 0 ].position ).toBe( 'top' );
|
|
153
|
+
expect( result.current.otherChildren ).toHaveLength( 0 );
|
|
154
|
+
} );
|
|
155
|
+
|
|
156
|
+
it( 'should default to bottom for invalid position values', () => {
|
|
157
|
+
// @ts-expect-error -- testing invalid runtime value
|
|
158
|
+
const children = <Legend position="left" />;
|
|
159
|
+
|
|
160
|
+
const { result } = renderHook( () => useChartChildren( children, 'TestChart' ) );
|
|
161
|
+
|
|
162
|
+
expect( result.current.legendChildren ).toHaveLength( 1 );
|
|
163
|
+
expect( result.current.legendChildren[ 0 ].position ).toBe( 'bottom' );
|
|
164
|
+
} );
|
|
165
|
+
|
|
166
|
+
it( 'should exclude Legend children from nonLegendChildren', () => {
|
|
167
|
+
const children = [
|
|
168
|
+
<Legend key="legend" position="top" />,
|
|
169
|
+
<div key="other">Other Content</div>,
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
const { result } = renderHook( () => useChartChildren( children, 'TestChart' ) );
|
|
173
|
+
|
|
174
|
+
expect( result.current.legendChildren ).toHaveLength( 1 );
|
|
175
|
+
expect( result.current.nonLegendChildren ).toHaveLength( 1 );
|
|
176
|
+
} );
|
|
177
|
+
|
|
178
|
+
it( 'should preserve original child order in nonLegendChildren', () => {
|
|
179
|
+
const children = [
|
|
180
|
+
<div key="first">First</div>,
|
|
181
|
+
<Legend key="legend" position="top" />,
|
|
182
|
+
<Group key="group">
|
|
183
|
+
<text>SVG</text>
|
|
184
|
+
</Group>,
|
|
185
|
+
<div key="last">Last</div>,
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
const { result } = renderHook( () => useChartChildren( children, 'TestChart' ) );
|
|
189
|
+
|
|
190
|
+
expect( result.current.nonLegendChildren ).toHaveLength( 3 );
|
|
191
|
+
// Verify order matches original (minus the Legend)
|
|
192
|
+
expect( ( result.current.nonLegendChildren[ 0 ] as React.ReactElement ).key ).toBe( 'first' );
|
|
193
|
+
expect( ( result.current.nonLegendChildren[ 1 ] as React.ReactElement ).key ).toBe( 'group' );
|
|
194
|
+
expect( ( result.current.nonLegendChildren[ 2 ] as React.ReactElement ).key ).toBe( 'last' );
|
|
195
|
+
} );
|
|
196
|
+
|
|
197
|
+
it( 'should return empty nonLegendChildren when all children are Legends', () => {
|
|
198
|
+
const children = [
|
|
199
|
+
<Legend key="top" position="top" />,
|
|
200
|
+
<Legend key="bottom" position="bottom" />,
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
const { result } = renderHook( () => useChartChildren( children, 'TestChart' ) );
|
|
204
|
+
|
|
205
|
+
expect( result.current.legendChildren ).toHaveLength( 2 );
|
|
206
|
+
expect( result.current.nonLegendChildren ).toHaveLength( 0 );
|
|
207
|
+
} );
|
|
208
|
+
|
|
209
|
+
it( 'should extract multiple Legend children by position', () => {
|
|
210
|
+
const children = [
|
|
211
|
+
<Legend key="top" position="top" />,
|
|
212
|
+
<Legend key="bottom" position="bottom" />,
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
const { result } = renderHook( () => useChartChildren( children, 'TestChart' ) );
|
|
216
|
+
|
|
217
|
+
expect( result.current.legendChildren ).toHaveLength( 2 );
|
|
218
|
+
expect( result.current.legendChildren[ 0 ].position ).toBe( 'top' );
|
|
219
|
+
expect( result.current.legendChildren[ 1 ].position ).toBe( 'bottom' );
|
|
220
|
+
expect( result.current.otherChildren ).toHaveLength( 0 );
|
|
221
|
+
} );
|
|
131
222
|
} );
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { Group } from '@visx/group';
|
|
2
2
|
import { useMemo, Children, isValidElement } from 'react';
|
|
3
|
-
import
|
|
3
|
+
import { Legend } from '../../../components/legend';
|
|
4
|
+
import type { LegendPosition } from '../../../types';
|
|
5
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
6
|
+
|
|
7
|
+
export type LegendChild = {
|
|
8
|
+
element: ReactElement;
|
|
9
|
+
position: LegendPosition;
|
|
10
|
+
};
|
|
4
11
|
|
|
5
12
|
interface ChartChildren {
|
|
6
13
|
svgChildren: ReactNode[];
|
|
7
14
|
htmlChildren: ReactNode[];
|
|
15
|
+
legendChildren: LegendChild[];
|
|
8
16
|
otherChildren: ReactNode[];
|
|
17
|
+
/** All children except Legend, in original order. */
|
|
18
|
+
nonLegendChildren: ReactNode[];
|
|
9
19
|
}
|
|
10
20
|
|
|
11
21
|
/**
|
|
@@ -21,10 +31,23 @@ export function useChartChildren( children: ReactNode, chartType: string ): Char
|
|
|
21
31
|
return useMemo( () => {
|
|
22
32
|
const svg: ReactNode[] = [];
|
|
23
33
|
const html: ReactNode[] = [];
|
|
34
|
+
const legend: LegendChild[] = [];
|
|
24
35
|
const other: ReactNode[] = [];
|
|
36
|
+
const nonLegend: ReactNode[] = [];
|
|
25
37
|
|
|
26
38
|
Children.forEach( children, child => {
|
|
27
39
|
if ( isValidElement( child ) ) {
|
|
40
|
+
// Extract Legend children for position-based slot rendering
|
|
41
|
+
if ( child.type === Legend ) {
|
|
42
|
+
const rawPosition = child.props?.position;
|
|
43
|
+
const position =
|
|
44
|
+
rawPosition === 'top' || rawPosition === 'bottom' ? rawPosition : 'bottom';
|
|
45
|
+
|
|
46
|
+
legend.push( { element: child as ReactElement, position } );
|
|
47
|
+
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
28
51
|
// Check displayName for compound components
|
|
29
52
|
const childType = child.type as { displayName?: string };
|
|
30
53
|
const displayName = childType?.displayName;
|
|
@@ -51,8 +74,17 @@ export function useChartChildren( children: ReactNode, chartType: string ): Char
|
|
|
51
74
|
other.push( child );
|
|
52
75
|
}
|
|
53
76
|
}
|
|
77
|
+
|
|
78
|
+
// Preserve original order of all non-Legend children
|
|
79
|
+
nonLegend.push( child );
|
|
54
80
|
} );
|
|
55
81
|
|
|
56
|
-
return {
|
|
82
|
+
return {
|
|
83
|
+
svgChildren: svg,
|
|
84
|
+
htmlChildren: html,
|
|
85
|
+
legendChildren: legend,
|
|
86
|
+
otherChildren: other,
|
|
87
|
+
nonLegendChildren: nonLegend,
|
|
88
|
+
};
|
|
57
89
|
}, [ children, chartType ] );
|
|
58
90
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Stack } from '@wordpress/ui';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { useElementSize } from '../../../hooks';
|
|
4
|
+
import { renderLegendSlot } from '../chart-composition';
|
|
5
|
+
import styles from './chart-layout.module.scss';
|
|
6
|
+
import type { LegendPosition } from '../../../types';
|
|
7
|
+
import type { LegendChild } from '../chart-composition/use-chart-children';
|
|
8
|
+
import type { GapSize } from '@wordpress/theme';
|
|
9
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Measurements provided to the render prop when ChartLayout handles resize listening.
|
|
13
|
+
*/
|
|
14
|
+
export interface ContentMeasurements {
|
|
15
|
+
/** Measured width of the content area in pixels */
|
|
16
|
+
contentWidth: number;
|
|
17
|
+
/** Measured height of the content area in pixels */
|
|
18
|
+
contentHeight: number;
|
|
19
|
+
/** True when a non-zero contentHeight measurement is available */
|
|
20
|
+
isMeasured: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ChartLayoutProps {
|
|
24
|
+
/** Position for the prop-based legend element */
|
|
25
|
+
legendPosition: LegendPosition;
|
|
26
|
+
/** The legend element rendered via the showLegend prop (false when hidden) */
|
|
27
|
+
legendElement?: ReactNode;
|
|
28
|
+
/** Legend children from the composition API */
|
|
29
|
+
legendChildren: LegendChild[];
|
|
30
|
+
/** Chart content — either a ReactNode or a render prop receiving content measurements */
|
|
31
|
+
children: ReactNode | ( ( measurements: ContentMeasurements ) => ReactNode );
|
|
32
|
+
/** Content rendered after the bottom legend (e.g., nonLegendChildren, htmlChildren, tooltips) */
|
|
33
|
+
trailingContent?: ReactNode;
|
|
34
|
+
/** Called when the measured content height changes (for render-prop mode) */
|
|
35
|
+
onContentHeightChange?: ( height: number ) => void;
|
|
36
|
+
/** Gap between Stack items */
|
|
37
|
+
gap?: GapSize;
|
|
38
|
+
/** Additional class names */
|
|
39
|
+
className?: string;
|
|
40
|
+
/** Inline styles (width, height, etc.) */
|
|
41
|
+
style?: CSSProperties;
|
|
42
|
+
/** Test ID for the container */
|
|
43
|
+
'data-testid'?: string;
|
|
44
|
+
/** Chart ID attribute */
|
|
45
|
+
'data-chart-id'?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const ChartLayout = ( {
|
|
49
|
+
legendPosition,
|
|
50
|
+
legendElement,
|
|
51
|
+
legendChildren,
|
|
52
|
+
children,
|
|
53
|
+
trailingContent,
|
|
54
|
+
onContentHeightChange,
|
|
55
|
+
gap,
|
|
56
|
+
className,
|
|
57
|
+
style,
|
|
58
|
+
'data-testid': dataTestId,
|
|
59
|
+
'data-chart-id': dataChartId,
|
|
60
|
+
}: ChartLayoutProps ) => {
|
|
61
|
+
const [ contentRef, contentWidth, contentHeight ] = useElementSize< HTMLDivElement >();
|
|
62
|
+
const isRenderProp = typeof children === 'function';
|
|
63
|
+
const isMeasured = contentHeight > 0;
|
|
64
|
+
|
|
65
|
+
// When using render-prop children, hide the layout until measurement is available
|
|
66
|
+
// to prevent layout shift. Plain ReactNode children don't need this since they
|
|
67
|
+
// don't depend on measured dimensions.
|
|
68
|
+
const visibilityStyle: { visibility?: 'hidden' | 'visible' } =
|
|
69
|
+
isRenderProp && ! isMeasured ? { visibility: 'hidden' } : {};
|
|
70
|
+
|
|
71
|
+
useEffect( () => {
|
|
72
|
+
if ( isRenderProp && onContentHeightChange && isMeasured ) {
|
|
73
|
+
onContentHeightChange( contentHeight );
|
|
74
|
+
}
|
|
75
|
+
}, [ isRenderProp, contentHeight, isMeasured, onContentHeightChange ] );
|
|
76
|
+
const renderedChildren = isRenderProp
|
|
77
|
+
? children( { contentWidth, contentHeight, isMeasured } )
|
|
78
|
+
: children;
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Stack
|
|
82
|
+
direction="column"
|
|
83
|
+
gap={ gap }
|
|
84
|
+
className={ className }
|
|
85
|
+
style={ { ...style, ...visibilityStyle } }
|
|
86
|
+
data-testid={ dataTestId }
|
|
87
|
+
data-chart-id={ dataChartId }
|
|
88
|
+
>
|
|
89
|
+
{ legendPosition === 'top' && legendElement }
|
|
90
|
+
{ renderLegendSlot( legendChildren, 'top' ) }
|
|
91
|
+
|
|
92
|
+
{ isRenderProp ? (
|
|
93
|
+
<div ref={ contentRef } className={ styles[ 'chart-layout__content' ] }>
|
|
94
|
+
{ renderedChildren }
|
|
95
|
+
</div>
|
|
96
|
+
) : (
|
|
97
|
+
renderedChildren
|
|
98
|
+
) }
|
|
99
|
+
|
|
100
|
+
{ legendPosition === 'bottom' && legendElement }
|
|
101
|
+
{ renderLegendSlot( legendChildren, 'bottom' ) }
|
|
102
|
+
|
|
103
|
+
{ trailingContent }
|
|
104
|
+
</Stack>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { renderLegendSlot } from '../../chart-composition';
|
|
3
|
+
import { ChartLayout } from '../chart-layout';
|
|
4
|
+
import type { LegendChild } from '../../chart-composition/use-chart-children';
|
|
5
|
+
|
|
6
|
+
// Mock renderLegendSlot since we test it separately
|
|
7
|
+
jest.mock( '../../chart-composition', () => ( {
|
|
8
|
+
renderLegendSlot: jest.fn( () => [] ),
|
|
9
|
+
} ) );
|
|
10
|
+
|
|
11
|
+
const mockRenderLegendSlot = renderLegendSlot as jest.Mock;
|
|
12
|
+
|
|
13
|
+
describe( 'ChartLayout', () => {
|
|
14
|
+
beforeEach( () => {
|
|
15
|
+
mockRenderLegendSlot.mockReturnValue( [] );
|
|
16
|
+
} );
|
|
17
|
+
|
|
18
|
+
it( 'renders children inside a column Stack', () => {
|
|
19
|
+
render(
|
|
20
|
+
<ChartLayout legendPosition="bottom" legendChildren={ [] }>
|
|
21
|
+
<div data-testid="chart-content">Chart</div>
|
|
22
|
+
</ChartLayout>
|
|
23
|
+
);
|
|
24
|
+
expect( screen.getByTestId( 'chart-content' ) ).toBeInTheDocument();
|
|
25
|
+
} );
|
|
26
|
+
|
|
27
|
+
it( 'renders legend element at top when legendPosition is top', () => {
|
|
28
|
+
const legendElement = <div data-testid="legend">Legend</div>;
|
|
29
|
+
render(
|
|
30
|
+
<ChartLayout legendPosition="top" legendElement={ legendElement } legendChildren={ [] }>
|
|
31
|
+
<div data-testid="chart-content">Chart</div>
|
|
32
|
+
</ChartLayout>
|
|
33
|
+
);
|
|
34
|
+
const legend = screen.getByTestId( 'legend' );
|
|
35
|
+
const content = screen.getByTestId( 'chart-content' );
|
|
36
|
+
expect( legend.compareDocumentPosition( content ) ).toBe( Node.DOCUMENT_POSITION_FOLLOWING );
|
|
37
|
+
} );
|
|
38
|
+
|
|
39
|
+
it( 'renders legend element at bottom when legendPosition is bottom', () => {
|
|
40
|
+
const legendElement = <div data-testid="legend">Legend</div>;
|
|
41
|
+
render(
|
|
42
|
+
<ChartLayout legendPosition="bottom" legendElement={ legendElement } legendChildren={ [] }>
|
|
43
|
+
<div data-testid="chart-content">Chart</div>
|
|
44
|
+
</ChartLayout>
|
|
45
|
+
);
|
|
46
|
+
const content = screen.getByTestId( 'chart-content' );
|
|
47
|
+
const legend = screen.getByTestId( 'legend' );
|
|
48
|
+
expect( content.compareDocumentPosition( legend ) ).toBe( Node.DOCUMENT_POSITION_FOLLOWING );
|
|
49
|
+
} );
|
|
50
|
+
|
|
51
|
+
it( 'does not render legend element when it is false/null', () => {
|
|
52
|
+
render(
|
|
53
|
+
<ChartLayout legendPosition="top" legendElement={ false } legendChildren={ [] }>
|
|
54
|
+
<div data-testid="chart-content">Chart</div>
|
|
55
|
+
</ChartLayout>
|
|
56
|
+
);
|
|
57
|
+
expect( screen.queryByTestId( 'legend' ) ).not.toBeInTheDocument();
|
|
58
|
+
} );
|
|
59
|
+
|
|
60
|
+
it( 'calls renderLegendSlot for both positions', () => {
|
|
61
|
+
const legendChildren: LegendChild[] = [];
|
|
62
|
+
render(
|
|
63
|
+
<ChartLayout legendPosition="bottom" legendChildren={ legendChildren }>
|
|
64
|
+
<div>Chart</div>
|
|
65
|
+
</ChartLayout>
|
|
66
|
+
);
|
|
67
|
+
expect( mockRenderLegendSlot ).toHaveBeenCalledWith( legendChildren, 'top' );
|
|
68
|
+
expect( mockRenderLegendSlot ).toHaveBeenCalledWith( legendChildren, 'bottom' );
|
|
69
|
+
} );
|
|
70
|
+
|
|
71
|
+
it( 'hides layout until measured when using render-prop children', () => {
|
|
72
|
+
// Override the global getBoundingClientRect mock to return zero (unmeasured state)
|
|
73
|
+
// for elements inside this test. This simulates the initial state before
|
|
74
|
+
// ResizeObserver provides real dimensions in a browser.
|
|
75
|
+
const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect;
|
|
76
|
+
Element.prototype.getBoundingClientRect = function () {
|
|
77
|
+
return { width: 0, height: 0, top: 0, left: 0, bottom: 0, right: 0, x: 0, y: 0 } as DOMRect;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const childFn = jest.fn().mockReturnValue( <div>Chart</div> );
|
|
82
|
+
render(
|
|
83
|
+
<ChartLayout legendPosition="bottom" legendChildren={ [] } data-testid="layout">
|
|
84
|
+
{ childFn }
|
|
85
|
+
</ChartLayout>
|
|
86
|
+
);
|
|
87
|
+
// When contentHeight is 0, layout should be hidden to prevent layout shift
|
|
88
|
+
expect( screen.getByTestId( 'layout' ) ).toHaveStyle( { visibility: 'hidden' } );
|
|
89
|
+
} finally {
|
|
90
|
+
Element.prototype.getBoundingClientRect = originalGetBoundingClientRect;
|
|
91
|
+
}
|
|
92
|
+
} );
|
|
93
|
+
|
|
94
|
+
it( 'does not hide layout when using plain ReactNode children', () => {
|
|
95
|
+
render(
|
|
96
|
+
<ChartLayout legendPosition="bottom" legendChildren={ [] } data-testid="layout">
|
|
97
|
+
<div>Chart</div>
|
|
98
|
+
</ChartLayout>
|
|
99
|
+
);
|
|
100
|
+
const layoutStyle = screen.getByTestId( 'layout' ).getAttribute( 'style' ) ?? '';
|
|
101
|
+
expect( layoutStyle ).not.toContain( 'visibility' );
|
|
102
|
+
} );
|
|
103
|
+
|
|
104
|
+
it( 'passes className and style to Stack', () => {
|
|
105
|
+
render(
|
|
106
|
+
<ChartLayout
|
|
107
|
+
legendPosition="bottom"
|
|
108
|
+
legendChildren={ [] }
|
|
109
|
+
className="my-chart"
|
|
110
|
+
style={ { width: 400, height: 300 } }
|
|
111
|
+
data-testid="layout"
|
|
112
|
+
>
|
|
113
|
+
<div>Chart</div>
|
|
114
|
+
</ChartLayout>
|
|
115
|
+
);
|
|
116
|
+
const layout = screen.getByTestId( 'layout' );
|
|
117
|
+
expect( layout ).toHaveClass( 'my-chart' );
|
|
118
|
+
expect( layout ).toHaveStyle( { width: '400px', height: '300px' } );
|
|
119
|
+
} );
|
|
120
|
+
|
|
121
|
+
it( 'passes gap to Stack', () => {
|
|
122
|
+
render(
|
|
123
|
+
<ChartLayout legendPosition="bottom" legendChildren={ [] } gap="lg" data-testid="layout">
|
|
124
|
+
<div>Chart</div>
|
|
125
|
+
</ChartLayout>
|
|
126
|
+
);
|
|
127
|
+
// Stack renders gap as a CSS class or style — just verify it renders without error
|
|
128
|
+
expect( screen.getByTestId( 'layout' ) ).toBeInTheDocument();
|
|
129
|
+
} );
|
|
130
|
+
|
|
131
|
+
it( 'calls function-as-children with measurement props', () => {
|
|
132
|
+
const childFn = jest.fn().mockReturnValue( <div data-testid="chart-content">Chart</div> );
|
|
133
|
+
|
|
134
|
+
render(
|
|
135
|
+
<ChartLayout legendPosition="bottom" legendChildren={ [] }>
|
|
136
|
+
{ childFn }
|
|
137
|
+
</ChartLayout>
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect( childFn ).toHaveBeenCalled();
|
|
141
|
+
const firstCallArg = childFn.mock.calls[ 0 ][ 0 ];
|
|
142
|
+
|
|
143
|
+
expect( firstCallArg ).toEqual(
|
|
144
|
+
expect.objectContaining( {
|
|
145
|
+
contentWidth: expect.any( Number ),
|
|
146
|
+
contentHeight: expect.any( Number ),
|
|
147
|
+
isMeasured: expect.any( Boolean ),
|
|
148
|
+
} )
|
|
149
|
+
);
|
|
150
|
+
} );
|
|
151
|
+
|
|
152
|
+
it( 'renders trailing content after bottom legend', () => {
|
|
153
|
+
render(
|
|
154
|
+
<ChartLayout
|
|
155
|
+
legendPosition="bottom"
|
|
156
|
+
legendElement={ <div data-testid="legend">Legend</div> }
|
|
157
|
+
legendChildren={ [] }
|
|
158
|
+
trailingContent={ <div data-testid="trailing">Extra</div> }
|
|
159
|
+
>
|
|
160
|
+
<div data-testid="chart-content">Chart</div>
|
|
161
|
+
</ChartLayout>
|
|
162
|
+
);
|
|
163
|
+
const legend = screen.getByTestId( 'legend' );
|
|
164
|
+
const trailing = screen.getByTestId( 'trailing' );
|
|
165
|
+
expect( legend.compareDocumentPosition( trailing ) ).toBe( Node.DOCUMENT_POSITION_FOLLOWING );
|
|
166
|
+
} );
|
|
167
|
+
} );
|
|
@@ -13,8 +13,8 @@ export interface ChartInstanceRef {
|
|
|
13
13
|
export interface ChartInstanceContextValue {
|
|
14
14
|
chartId: string;
|
|
15
15
|
chartRef?: React.RefObject< ChartInstanceRef >;
|
|
16
|
-
chartWidth
|
|
17
|
-
chartHeight
|
|
16
|
+
chartWidth?: number;
|
|
17
|
+
chartHeight?: number;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export const ChartInstanceContext = createContext< ChartInstanceContextValue | null >( null );
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SvgEmptyState } from './svg-empty-state';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Stack } from '@wordpress/ui';
|
|
2
|
+
import styles from './svg-empty-state.module.scss';
|
|
3
|
+
import type { FC, ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
interface SvgEmptyStateProps {
|
|
6
|
+
/** X coordinate of the center point */
|
|
7
|
+
x: number;
|
|
8
|
+
/** Y coordinate of the center point */
|
|
9
|
+
y: number;
|
|
10
|
+
/** Available width for the text area */
|
|
11
|
+
width: number;
|
|
12
|
+
/** Available height for the text area */
|
|
13
|
+
height: number;
|
|
14
|
+
/** Text content */
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Renders empty-state text inside an SVG using foreignObject so that the
|
|
20
|
+
* message wraps onto multiple lines instead of being clipped.
|
|
21
|
+
*
|
|
22
|
+
* The component centers the text within the specified area.
|
|
23
|
+
*
|
|
24
|
+
* @param root0 - Component props
|
|
25
|
+
* @param root0.x - X coordinate of the center point
|
|
26
|
+
* @param root0.y - Y coordinate of the center point
|
|
27
|
+
* @param root0.width - Available width for the text area
|
|
28
|
+
* @param root0.height - Available height for the text area
|
|
29
|
+
* @param root0.children - Text content
|
|
30
|
+
* @return {JSX.Element} A foreignObject element containing the centered text.
|
|
31
|
+
*/
|
|
32
|
+
export const SvgEmptyState: FC< SvgEmptyStateProps > = ( { x, y, width, height, children } ) => {
|
|
33
|
+
return (
|
|
34
|
+
<foreignObject x={ x - width / 2 } y={ y - height / 2 } width={ width } height={ height }>
|
|
35
|
+
<Stack align="center" justify="center" className={ styles[ 'svg-empty-state' ] }>
|
|
36
|
+
{ children }
|
|
37
|
+
</Stack>
|
|
38
|
+
</foreignObject>
|
|
39
|
+
);
|
|
40
|
+
};
|