@automattic/charts 1.10.1 → 1.10.3
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 +17 -0
- package/dist/index.cjs +248 -226
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +36 -7
- package/dist/index.d.cts +44 -5
- package/dist/index.d.ts +44 -5
- package/dist/index.js +236 -202
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +23 -0
- package/src/charts/heatmap-chart/heatmap-chart.tsx +78 -13
- package/src/charts/heatmap-chart/private/build-calendar-data.ts +22 -3
- package/src/charts/heatmap-chart/private/use-heatmap-colors.ts +6 -2
- package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +34 -0
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +81 -0
- package/src/charts/heatmap-chart/test/use-heatmap-colors.test.ts +4 -0
- package/src/charts/heatmap-chart/types.ts +28 -0
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +10 -10
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +6 -3
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +0 -6
- package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +0 -6
- package/src/charts/private/with-responsive/test/with-responsive.test.tsx +229 -18
- package/src/charts/private/with-responsive/with-responsive.module.scss +20 -1
- package/src/charts/private/with-responsive/with-responsive.tsx +114 -65
|
@@ -5,7 +5,7 @@ import type { BaseChartProps } from '../../../../types';
|
|
|
5
5
|
// Mock the useParentSize hook
|
|
6
6
|
jest.mock( '@visx/responsive', () => ( {
|
|
7
7
|
useParentSize: jest.fn( () => ( {
|
|
8
|
-
parentRef:
|
|
8
|
+
parentRef: () => {},
|
|
9
9
|
width: 600, // Default width for tests
|
|
10
10
|
height: 300, // Default height for tests
|
|
11
11
|
} ) ),
|
|
@@ -20,6 +20,32 @@ describe( 'withResponsive', () => {
|
|
|
20
20
|
|
|
21
21
|
const ResponsiveComponent = withResponsive( MockComponent );
|
|
22
22
|
|
|
23
|
+
const { useParentSize } = jest.requireMock( '@visx/responsive' );
|
|
24
|
+
const DEFAULT_SIZE = { parentRef: () => {}, width: 600, height: 300 };
|
|
25
|
+
|
|
26
|
+
// jsdom has no layout engine, so the wrapper's clientHeight (which the contain
|
|
27
|
+
// logic reads from real layout) is always 0. Mock it to simulate the height the
|
|
28
|
+
// parent makes available: 0 means unconstrained (the chart derives its height from
|
|
29
|
+
// width), a positive value simulates a parent that limits the height.
|
|
30
|
+
let mockClientHeight = 0;
|
|
31
|
+
beforeAll( () => {
|
|
32
|
+
Object.defineProperty( window.HTMLElement.prototype, 'clientHeight', {
|
|
33
|
+
configurable: true,
|
|
34
|
+
get() {
|
|
35
|
+
return mockClientHeight;
|
|
36
|
+
},
|
|
37
|
+
} );
|
|
38
|
+
} );
|
|
39
|
+
|
|
40
|
+
// Fully clear the mock (queued returns and implementation) after each test, then
|
|
41
|
+
// re-establish the default implementation, so a per-test override can't bleed
|
|
42
|
+
// into the next test.
|
|
43
|
+
afterEach( () => {
|
|
44
|
+
mockClientHeight = 0;
|
|
45
|
+
useParentSize.mockReset();
|
|
46
|
+
useParentSize.mockImplementation( () => DEFAULT_SIZE );
|
|
47
|
+
} );
|
|
48
|
+
|
|
23
49
|
describe( 'component dimensions', () => {
|
|
24
50
|
test( 'passes measured parent width to component', () => {
|
|
25
51
|
render( <ResponsiveComponent data={ [] } /> );
|
|
@@ -34,11 +60,35 @@ describe( 'withResponsive', () => {
|
|
|
34
60
|
expect( component ).toHaveStyle( { height: '300px' } );
|
|
35
61
|
} );
|
|
36
62
|
|
|
37
|
-
test( '
|
|
63
|
+
test( 'derives height from width when the parent is taller than the derived height', () => {
|
|
64
|
+
// Parent allows 300px of height; aspectRatio 0.4 derives 600 * 0.4 = 240,
|
|
65
|
+
// which fits, so the chart keeps the full measured width.
|
|
66
|
+
mockClientHeight = 300;
|
|
67
|
+
render( <ResponsiveComponent data={ [] } aspectRatio={ 0.4 } /> );
|
|
68
|
+
const component = screen.getByTestId( 'mock-component' );
|
|
69
|
+
expect( component ).toHaveStyle( { width: '600px', height: '240px' } );
|
|
70
|
+
} );
|
|
71
|
+
|
|
72
|
+
test( 'contains within the parent height when the parent limits the available height', () => {
|
|
73
|
+
// Parent makes 300px of height available; aspectRatio 0.75 would derive
|
|
74
|
+
// 600 * 0.75 = 450, so both axes shrink to fit: 300 / 0.75 = 400.
|
|
75
|
+
mockClientHeight = 300;
|
|
38
76
|
render( <ResponsiveComponent data={ [] } aspectRatio={ 0.75 } /> );
|
|
39
77
|
const component = screen.getByTestId( 'mock-component' );
|
|
40
|
-
|
|
41
|
-
|
|
78
|
+
expect( component ).toHaveStyle( { width: '400px', height: '300px' } );
|
|
79
|
+
} );
|
|
80
|
+
|
|
81
|
+
test( 'derives height from width when the parent has no measured height', () => {
|
|
82
|
+
// parentHeight 0 (unconstrained or not-yet-measured parent): the contain
|
|
83
|
+
// clamp must not run, so the height stays width-derived (600 * 0.4 = 240).
|
|
84
|
+
useParentSize.mockImplementation( () => ( {
|
|
85
|
+
parentRef: () => {},
|
|
86
|
+
width: 600,
|
|
87
|
+
height: 0,
|
|
88
|
+
} ) );
|
|
89
|
+
render( <ResponsiveComponent data={ [] } aspectRatio={ 0.4 } /> );
|
|
90
|
+
const component = screen.getByTestId( 'mock-component' );
|
|
91
|
+
expect( component ).toHaveStyle( { width: '600px', height: '240px' } );
|
|
42
92
|
} );
|
|
43
93
|
|
|
44
94
|
test( 'respects maxWidth configuration', () => {
|
|
@@ -52,13 +102,28 @@ describe( 'withResponsive', () => {
|
|
|
52
102
|
const component = screen.getByTestId( 'mock-component' );
|
|
53
103
|
expect( component ).toHaveAttribute( 'data-size', '200' );
|
|
54
104
|
} );
|
|
105
|
+
|
|
106
|
+
test( 'assigns the wrapper to an object-shaped parentRef from useParentSize', () => {
|
|
107
|
+
// useParentSize hands back a callback ref in practice, but older versions
|
|
108
|
+
// return a RefObject; the wrapper must populate that shape too.
|
|
109
|
+
const objectRef: { current: HTMLElement | null } = { current: null };
|
|
110
|
+
useParentSize.mockImplementation( () => ( {
|
|
111
|
+
parentRef: objectRef,
|
|
112
|
+
width: 600,
|
|
113
|
+
height: 300,
|
|
114
|
+
} ) );
|
|
115
|
+
render( <ResponsiveComponent data={ [] } /> );
|
|
116
|
+
expect( objectRef.current ).toBe( screen.getByTestId( 'responsive-wrapper' ) );
|
|
117
|
+
} );
|
|
55
118
|
} );
|
|
56
119
|
|
|
57
120
|
describe( 'wrapper dimensions', () => {
|
|
58
|
-
test( 'wrapper
|
|
121
|
+
test( 'wrapper has no inline dimensions by default (fills the parent via CSS)', () => {
|
|
59
122
|
render( <ResponsiveComponent data={ [] } /> );
|
|
60
123
|
const wrapper = screen.getByTestId( 'responsive-wrapper' );
|
|
61
|
-
expect( wrapper ).
|
|
124
|
+
expect( wrapper ).toHaveClass( 'container' );
|
|
125
|
+
expect( wrapper ).not.toHaveClass( 'isContained' );
|
|
126
|
+
expect( wrapper ).not.toHaveAttribute( 'style' );
|
|
62
127
|
} );
|
|
63
128
|
|
|
64
129
|
test( 'wrapper uses explicit width/height for dimensions when provided', () => {
|
|
@@ -67,24 +132,170 @@ describe( 'withResponsive', () => {
|
|
|
67
132
|
expect( wrapper ).toHaveStyle( { width: '200px', height: '200px' } );
|
|
68
133
|
} );
|
|
69
134
|
|
|
70
|
-
test( 'wrapper
|
|
135
|
+
test( 'wrapper gets the contained class with aspectRatio (centering lives in CSS)', () => {
|
|
71
136
|
render( <ResponsiveComponent data={ [] } aspectRatio={ 0.5 } /> );
|
|
72
137
|
const wrapper = screen.getByTestId( 'responsive-wrapper' );
|
|
73
|
-
expect( wrapper ).
|
|
138
|
+
expect( wrapper ).toHaveClass( 'isContained' );
|
|
74
139
|
} );
|
|
75
140
|
|
|
76
|
-
test( '
|
|
77
|
-
render( <ResponsiveComponent data={ [] }
|
|
78
|
-
|
|
79
|
-
// CSS aspect-ratio is width/height, so 1 / 0.5 = 2.
|
|
80
|
-
expect( wrapper ).toHaveStyle( { aspectRatio: '2', maxWidth: '800px' } );
|
|
141
|
+
test( 'no content box is rendered without an aspectRatio', () => {
|
|
142
|
+
render( <ResponsiveComponent data={ [] } /> );
|
|
143
|
+
expect( screen.queryByTestId( 'responsive-content' ) ).not.toBeInTheDocument();
|
|
81
144
|
} );
|
|
82
145
|
|
|
83
|
-
test( '
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
expect(
|
|
146
|
+
test( 'renders the contained chart in an inner content box sized to the aspect ratio', () => {
|
|
147
|
+
// aspectRatio 0.4: 600 * 0.4 = 240 fits the 300px parent.
|
|
148
|
+
render( <ResponsiveComponent data={ [] } aspectRatio={ 0.4 } /> );
|
|
149
|
+
const content = screen.getByTestId( 'responsive-content' );
|
|
150
|
+
expect( content ).toHaveStyle( { width: '600px', height: '240px' } );
|
|
151
|
+
} );
|
|
152
|
+
|
|
153
|
+
test( 'content box contains to the parent height when the parent limits the available height', () => {
|
|
154
|
+
// aspectRatio 0.75 derives 450 > the 300px the parent allows, so both axes
|
|
155
|
+
// shrink to fit: width 400 (300 / 0.75) × height 300.
|
|
156
|
+
mockClientHeight = 300;
|
|
157
|
+
render( <ResponsiveComponent data={ [] } aspectRatio={ 0.75 } /> );
|
|
158
|
+
const content = screen.getByTestId( 'responsive-content' );
|
|
159
|
+
expect( content ).toHaveStyle( { width: '400px', height: '300px' } );
|
|
160
|
+
} );
|
|
161
|
+
} );
|
|
162
|
+
|
|
163
|
+
describe( 'resize behavior', () => {
|
|
164
|
+
test( 'grows on widen in a fluid (unconstrained-height) parent', () => {
|
|
165
|
+
// Unconstrained parent (mockClientHeight 0): the chart derives its height
|
|
166
|
+
// from width and must grow when the parent widens, not deadlock.
|
|
167
|
+
useParentSize.mockImplementation( () => ( {
|
|
168
|
+
parentRef: () => {},
|
|
169
|
+
width: 600,
|
|
170
|
+
height: 0,
|
|
171
|
+
} ) );
|
|
172
|
+
const { rerender } = render( <ResponsiveComponent data={ [] } aspectRatio={ 0.4 } /> );
|
|
173
|
+
expect( screen.getByTestId( 'mock-component' ) ).toHaveStyle( {
|
|
174
|
+
width: '600px',
|
|
175
|
+
height: '240px',
|
|
176
|
+
} );
|
|
177
|
+
|
|
178
|
+
useParentSize.mockImplementation( () => ( {
|
|
179
|
+
parentRef: () => {},
|
|
180
|
+
width: 900,
|
|
181
|
+
height: 0,
|
|
182
|
+
} ) );
|
|
183
|
+
rerender( <ResponsiveComponent data={ [] } aspectRatio={ 0.4 } /> );
|
|
184
|
+
expect( screen.getByTestId( 'mock-component' ) ).toHaveStyle( {
|
|
185
|
+
width: '900px',
|
|
186
|
+
height: '360px',
|
|
187
|
+
} );
|
|
188
|
+
} );
|
|
189
|
+
|
|
190
|
+
test( 'stays contained on widen when the parent limits the height', () => {
|
|
191
|
+
// Parent allows 150px of height. aspectRatio 0.4 derives 240 at width 600 and
|
|
192
|
+
// 360 at width 900 — both exceed 150 — so the chart stays contained at
|
|
193
|
+
// 375 × 150 (150 / 0.4 = 375) across the widen rather than overflowing.
|
|
194
|
+
mockClientHeight = 150;
|
|
195
|
+
useParentSize.mockImplementation( () => ( {
|
|
196
|
+
parentRef: () => {},
|
|
197
|
+
width: 600,
|
|
198
|
+
height: 150,
|
|
199
|
+
} ) );
|
|
200
|
+
const { rerender } = render( <ResponsiveComponent data={ [] } aspectRatio={ 0.4 } /> );
|
|
201
|
+
expect( screen.getByTestId( 'mock-component' ) ).toHaveStyle( {
|
|
202
|
+
width: '375px',
|
|
203
|
+
height: '150px',
|
|
204
|
+
} );
|
|
205
|
+
|
|
206
|
+
useParentSize.mockImplementation( () => ( {
|
|
207
|
+
parentRef: () => {},
|
|
208
|
+
width: 900,
|
|
209
|
+
height: 150,
|
|
210
|
+
} ) );
|
|
211
|
+
rerender( <ResponsiveComponent data={ [] } aspectRatio={ 0.4 } /> );
|
|
212
|
+
expect( screen.getByTestId( 'mock-component' ) ).toHaveStyle( {
|
|
213
|
+
width: '375px',
|
|
214
|
+
height: '150px',
|
|
215
|
+
} );
|
|
216
|
+
} );
|
|
217
|
+
|
|
218
|
+
test( 'releases containment when the parent grows tall enough', () => {
|
|
219
|
+
// Parent starts at 150px of height, so aspectRatio 0.4 (derives 240 at width
|
|
220
|
+
// 600) is contained to 375 × 150.
|
|
221
|
+
mockClientHeight = 150;
|
|
222
|
+
useParentSize.mockImplementation( () => ( {
|
|
223
|
+
parentRef: () => {},
|
|
224
|
+
width: 600,
|
|
225
|
+
height: 150,
|
|
226
|
+
} ) );
|
|
227
|
+
const { rerender } = render( <ResponsiveComponent data={ [] } aspectRatio={ 0.4 } /> );
|
|
228
|
+
expect( screen.getByTestId( 'mock-component' ) ).toHaveStyle( {
|
|
229
|
+
width: '375px',
|
|
230
|
+
height: '150px',
|
|
231
|
+
} );
|
|
232
|
+
|
|
233
|
+
// Parent grows to 300px — now taller than the 240px derived height — so
|
|
234
|
+
// containment releases and the chart returns to its full width-derived size.
|
|
235
|
+
mockClientHeight = 300;
|
|
236
|
+
useParentSize.mockImplementation( () => ( {
|
|
237
|
+
parentRef: () => {},
|
|
238
|
+
width: 600,
|
|
239
|
+
height: 300,
|
|
240
|
+
} ) );
|
|
241
|
+
rerender( <ResponsiveComponent data={ [] } aspectRatio={ 0.4 } /> );
|
|
242
|
+
expect( screen.getByTestId( 'mock-component' ) ).toHaveStyle( {
|
|
243
|
+
width: '600px',
|
|
244
|
+
height: '240px',
|
|
245
|
+
} );
|
|
246
|
+
} );
|
|
247
|
+
|
|
248
|
+
test( 'and re-contains to the new height when the parent shrinks further', () => {
|
|
249
|
+
// Parent allows 300px; aspectRatio 0.75 derives 450, so it contains to 400 × 300.
|
|
250
|
+
mockClientHeight = 300;
|
|
251
|
+
useParentSize.mockImplementation( () => ( {
|
|
252
|
+
parentRef: () => {},
|
|
253
|
+
width: 600,
|
|
254
|
+
height: 300,
|
|
255
|
+
} ) );
|
|
256
|
+
const { rerender } = render( <ResponsiveComponent data={ [] } aspectRatio={ 0.75 } /> );
|
|
257
|
+
expect( screen.getByTestId( 'mock-component' ) ).toHaveStyle( {
|
|
258
|
+
width: '400px',
|
|
259
|
+
height: '300px',
|
|
260
|
+
} );
|
|
261
|
+
|
|
262
|
+
// Parent shrinks to 150px — still shorter than the 450px derived height — so the
|
|
263
|
+
// chart re-contains to the new available height: 150 / 0.75 = 200 × 150.
|
|
264
|
+
mockClientHeight = 150;
|
|
265
|
+
useParentSize.mockImplementation( () => ( {
|
|
266
|
+
parentRef: () => {},
|
|
267
|
+
width: 600,
|
|
268
|
+
height: 150,
|
|
269
|
+
} ) );
|
|
270
|
+
rerender( <ResponsiveComponent data={ [] } aspectRatio={ 0.75 } /> );
|
|
271
|
+
expect( screen.getByTestId( 'mock-component' ) ).toHaveStyle( {
|
|
272
|
+
width: '200px',
|
|
273
|
+
height: '150px',
|
|
274
|
+
} );
|
|
275
|
+
} );
|
|
276
|
+
|
|
277
|
+
test( 'releases containment when aspectRatio is removed', () => {
|
|
278
|
+
// Contained first (derived 450 > the 300px parent → 400 × 300).
|
|
279
|
+
mockClientHeight = 300;
|
|
280
|
+
useParentSize.mockImplementation( () => ( {
|
|
281
|
+
parentRef: () => {},
|
|
282
|
+
width: 600,
|
|
283
|
+
height: 300,
|
|
284
|
+
} ) );
|
|
285
|
+
const { rerender } = render( <ResponsiveComponent data={ [] } aspectRatio={ 0.75 } /> );
|
|
286
|
+
expect( screen.getByTestId( 'mock-component' ) ).toHaveStyle( {
|
|
287
|
+
width: '400px',
|
|
288
|
+
height: '300px',
|
|
289
|
+
} );
|
|
290
|
+
|
|
291
|
+
// Dropping aspectRatio clears the containment and fills the parent again; the
|
|
292
|
+
// inner content box is gone.
|
|
293
|
+
rerender( <ResponsiveComponent data={ [] } /> );
|
|
294
|
+
expect( screen.queryByTestId( 'responsive-content' ) ).not.toBeInTheDocument();
|
|
295
|
+
expect( screen.getByTestId( 'mock-component' ) ).toHaveStyle( {
|
|
296
|
+
width: '600px',
|
|
297
|
+
height: '300px',
|
|
298
|
+
} );
|
|
88
299
|
} );
|
|
89
300
|
} );
|
|
90
301
|
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
.container {
|
|
2
|
-
//
|
|
2
|
+
// Fill the parent by default so the wrapper's live height reflects the
|
|
3
|
+
// real available space. JS sets width/height only when passed explicitly.
|
|
4
|
+
width: 100%;
|
|
5
|
+
height: 100%;
|
|
6
|
+
// Allow the wrapper to shrink in flex layouts
|
|
7
|
+
min-width: 0;
|
|
8
|
+
min-height: 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// With an aspectRatio, center the contained box horizontally and top-align it.
|
|
12
|
+
.isContained {
|
|
13
|
+
display: flex;
|
|
14
|
+
justify-content: center;
|
|
15
|
+
align-items: flex-start;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// The contain box. Its width/height come from JS; keep it from being stretched
|
|
19
|
+
// or shrunk by the flex parent.
|
|
20
|
+
.content {
|
|
21
|
+
flex: 0 0 auto;
|
|
3
22
|
min-width: 0;
|
|
4
23
|
min-height: 0;
|
|
5
24
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { useParentSize } from '@visx/responsive';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
4
|
import styles from './with-responsive.module.scss';
|
|
3
5
|
import type { BaseChartProps } from '../../../types';
|
|
4
6
|
import type { ComponentType } from 'react';
|
|
@@ -16,7 +18,11 @@ export type ResponsiveConfig = {
|
|
|
16
18
|
maxWidth?: number;
|
|
17
19
|
/**
|
|
18
20
|
* The aspect ratio of the chart (height = width * aspectRatio).
|
|
19
|
-
* When provided,
|
|
21
|
+
* When provided, the chart keeps this ratio and is contained within the
|
|
22
|
+
* parent on both axes: it fills the available width and derives its height,
|
|
23
|
+
* but if the parent is shorter than that derived height it shrinks both axes
|
|
24
|
+
* to fit rather than overflowing. When it is narrower than the parent (the
|
|
25
|
+
* height-constrained case) it is centered horizontally.
|
|
20
26
|
* When omitted, the chart fills the parent container's height.
|
|
21
27
|
*/
|
|
22
28
|
aspectRatio?: number;
|
|
@@ -26,35 +32,9 @@ export type ResponsiveConfig = {
|
|
|
26
32
|
resizeDebounceTime?: number;
|
|
27
33
|
};
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
aspectRatio,
|
|
33
|
-
}: ResponsiveConfig ) => {
|
|
34
|
-
const {
|
|
35
|
-
parentRef,
|
|
36
|
-
width: parentWidth,
|
|
37
|
-
height: parentHeight,
|
|
38
|
-
} = useParentSize( {
|
|
39
|
-
debounceTime: resizeDebounceTime,
|
|
40
|
-
enableDebounceLeadingCall: true,
|
|
41
|
-
} );
|
|
42
|
-
|
|
43
|
-
const containerWidth = parentWidth > 0 ? Math.min( parentWidth, maxWidth ) : 0;
|
|
44
|
-
const containerHeight = aspectRatio !== undefined ? containerWidth * aspectRatio : parentHeight;
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
parentRef,
|
|
48
|
-
width: containerWidth,
|
|
49
|
-
height: containerHeight,
|
|
50
|
-
/**
|
|
51
|
-
* Whether an aspectRatio was provided. Used to determine container
|
|
52
|
-
* height styling: 'auto' when true (height derived from width),
|
|
53
|
-
* '100%' when false (fill parent container).
|
|
54
|
-
*/
|
|
55
|
-
hasAspectRatio: aspectRatio !== undefined,
|
|
56
|
-
};
|
|
57
|
-
};
|
|
35
|
+
// useLayoutEffect on the client (so containment is resolved before paint, no flash),
|
|
36
|
+
// useEffect on the server to avoid React's SSR warning.
|
|
37
|
+
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
|
58
38
|
|
|
59
39
|
/**
|
|
60
40
|
* A higher-order component that provides responsive dimensions
|
|
@@ -77,51 +57,120 @@ export function withResponsive< T extends Exclude< BaseChartProps< unknown >, 'o
|
|
|
77
57
|
}: Omit< T, 'width' | 'height' | 'size' > & DimensionProps & ResponsiveConfig ) {
|
|
78
58
|
const {
|
|
79
59
|
parentRef,
|
|
80
|
-
width:
|
|
81
|
-
height:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
maxWidth,
|
|
86
|
-
aspectRatio,
|
|
60
|
+
width: parentWidth,
|
|
61
|
+
height: parentHeight,
|
|
62
|
+
} = useParentSize( {
|
|
63
|
+
debounceTime: resizeDebounceTime,
|
|
64
|
+
enableDebounceLeadingCall: true,
|
|
87
65
|
} );
|
|
88
66
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
67
|
+
const hasAspectRatio = aspectRatio !== undefined && aspectRatio > 0;
|
|
68
|
+
|
|
69
|
+
// Keep our own handle on the wrapper so we can read its live height below, and
|
|
70
|
+
// still hand the node to useParentSize's ref (a callback ref in practice; guard
|
|
71
|
+
// the object-ref shape too).
|
|
72
|
+
const wrapperRef = useRef< HTMLDivElement | null >( null );
|
|
73
|
+
const setWrapperRef = useCallback(
|
|
74
|
+
( node: HTMLDivElement | null ) => {
|
|
75
|
+
wrapperRef.current = node;
|
|
76
|
+
if ( typeof parentRef === 'function' ) {
|
|
77
|
+
parentRef( node );
|
|
78
|
+
} else if ( parentRef ) {
|
|
79
|
+
( parentRef as { current: HTMLDivElement | null } ).current = node;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
[ parentRef ]
|
|
83
|
+
);
|
|
94
84
|
|
|
95
|
-
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
85
|
+
// The parent's available height when it actually constrains the chart, else
|
|
86
|
+
// null (the chart derives its height from width and grows freely).
|
|
87
|
+
const [ containedHeight, setContainedHeight ] = useState< number | null >( null );
|
|
88
|
+
|
|
89
|
+
// Cap the available width at maxWidth unless an explicit width pins it. Before
|
|
90
|
+
// measurement resolves, fall back to the explicit width.
|
|
91
|
+
const cap = width === undefined ? maxWidth : Infinity;
|
|
92
|
+
const availableWidth = parentWidth > 0 ? Math.min( parentWidth, cap ) : width ?? 0;
|
|
93
|
+
|
|
94
|
+
let boxWidth = availableWidth;
|
|
95
|
+
let boxHeight: number;
|
|
96
|
+
if ( hasAspectRatio ) {
|
|
97
|
+
const derivedHeight = availableWidth * aspectRatio;
|
|
98
|
+
if ( containedHeight !== null && derivedHeight > containedHeight ) {
|
|
99
|
+
boxHeight = containedHeight;
|
|
100
|
+
boxWidth = boxHeight / aspectRatio;
|
|
101
|
+
} else {
|
|
102
|
+
boxHeight = derivedHeight;
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
boxHeight = parentHeight > 0 ? parentHeight : height ?? 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Decide containment from the wrapper's real height, measured after layout.
|
|
109
|
+
// useParentSize's height is no good here: with height:100% and no definite
|
|
110
|
+
// parent height it just echoes our own content back, so clamping against it
|
|
111
|
+
// would freeze the chart at its current width when the container widens.
|
|
112
|
+
// Compare the post-layout clientHeight to the height we drew: shorter means the
|
|
113
|
+
// parent really constrains us → contain to it; equal means it is only our own
|
|
114
|
+
// content → let it grow. Once contained, stay contained until the parent is tall
|
|
115
|
+
// enough for the full derived height, so a now-fitting chart doesn't oscillate.
|
|
116
|
+
// (parentHeight is in the deps only to re-run this; the value used is clientHeight.)
|
|
117
|
+
useIsomorphicLayoutEffect( () => {
|
|
118
|
+
if ( ! hasAspectRatio ) {
|
|
119
|
+
if ( containedHeight !== null ) {
|
|
120
|
+
setContainedHeight( null );
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const available = wrapperRef.current?.clientHeight ?? 0;
|
|
125
|
+
const derivedHeight = availableWidth * aspectRatio;
|
|
126
|
+
if ( containedHeight === null ) {
|
|
127
|
+
if ( available > 0 && derivedHeight > available + 1 ) {
|
|
128
|
+
setContainedHeight( available );
|
|
129
|
+
}
|
|
130
|
+
} else if ( available >= derivedHeight - 1 ) {
|
|
131
|
+
setContainedHeight( null );
|
|
132
|
+
} else if ( Math.abs( available - containedHeight ) > 1 ) {
|
|
133
|
+
setContainedHeight( available );
|
|
134
|
+
}
|
|
135
|
+
}, [ hasAspectRatio, availableWidth, aspectRatio, containedHeight, parentHeight ] );
|
|
136
|
+
|
|
137
|
+
const wrappedComponent = (
|
|
138
|
+
<WrappedComponent
|
|
139
|
+
width={ boxWidth }
|
|
140
|
+
height={ boxHeight }
|
|
141
|
+
size={ size }
|
|
142
|
+
{ ...( chartProps as T ) }
|
|
143
|
+
/>
|
|
144
|
+
);
|
|
107
145
|
|
|
146
|
+
// The outer element fills the parent (via CSS) so its live height reflects the
|
|
147
|
+
// real available space; JS sets width/height only when they are passed
|
|
148
|
+
// explicitly. Static layout (fill, flex centering) lives in the stylesheet. With
|
|
149
|
+
// an aspectRatio the chart is placed in an inner box sized to the contained
|
|
150
|
+
// dimensions (centered by CSS) so it keeps its proportions and fits within the
|
|
151
|
+
// parent on both axes. Charts that fill their container via CSS (e.g. the heatmap
|
|
152
|
+
// grid) then track that box. Without an aspectRatio the chart fills the parent.
|
|
108
153
|
return (
|
|
109
154
|
<div
|
|
110
|
-
ref={
|
|
155
|
+
ref={ setWrapperRef }
|
|
111
156
|
data-testid="responsive-wrapper"
|
|
112
|
-
className={ styles.container }
|
|
157
|
+
className={ clsx( styles.container, hasAspectRatio && styles.isContained ) }
|
|
113
158
|
style={ {
|
|
114
|
-
width
|
|
115
|
-
height
|
|
116
|
-
...aspectRatioStyle,
|
|
159
|
+
...( width !== undefined ? { width } : null ),
|
|
160
|
+
...( height !== undefined ? { height } : null ),
|
|
117
161
|
} }
|
|
118
162
|
>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
163
|
+
{ hasAspectRatio ? (
|
|
164
|
+
<div
|
|
165
|
+
data-testid="responsive-content"
|
|
166
|
+
className={ styles.content }
|
|
167
|
+
style={ { width: boxWidth, height: boxHeight } }
|
|
168
|
+
>
|
|
169
|
+
{ wrappedComponent }
|
|
170
|
+
</div>
|
|
171
|
+
) : (
|
|
172
|
+
wrappedComponent
|
|
173
|
+
) }
|
|
125
174
|
</div>
|
|
126
175
|
);
|
|
127
176
|
};
|