@react-magma/charts 13.0.4-next.0 → 14.0.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/charts.js +447 -27
- package/dist/charts.js.map +1 -1
- package/dist/charts.modern.module.js +443 -30
- package/dist/charts.modern.module.js.map +1 -1
- package/dist/charts.umd.js +1303 -153
- package/dist/charts.umd.js.map +1 -1
- package/dist/components/CarbonChart/CarbonChart.d.ts +41 -0
- package/dist/components/ChartTable/ChartDataTable.d.ts +19 -0
- package/dist/components/ChartTable/ChartFullscreenButton.d.ts +26 -0
- package/dist/components/ChartTable/ChartMoreOptionsButton.d.ts +18 -0
- package/dist/components/ChartTable/ChartTable.stories.d.ts +116 -0
- package/dist/components/ChartTable/ChartTable.test.d.ts +1 -0
- package/dist/components/ChartTable/ChartTableButton.d.ts +24 -0
- package/dist/components/ChartTable/ChartTableModal.d.ts +44 -0
- package/dist/components/ChartTable/ChartToolbar.d.ts +19 -0
- package/dist/components/ChartTable/chartToolbarI18n.d.ts +16 -0
- package/dist/components/ChartTable/index.d.ts +14 -0
- package/dist/components/LineChart/DataTable.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/package.json +5 -5
- package/src/components/CarbonChart/CarbonChart.test.js +143 -2
- package/src/components/CarbonChart/CarbonChart.tsx +603 -15
- package/src/components/ChartTable/ChartDataTable.tsx +72 -0
- package/src/components/ChartTable/ChartFullscreenButton.tsx +59 -0
- package/src/components/ChartTable/ChartMoreOptionsButton.tsx +47 -0
- package/src/components/ChartTable/ChartTable.stories.tsx +152 -0
- package/src/components/ChartTable/ChartTable.test.tsx +444 -0
- package/src/components/ChartTable/ChartTableButton.tsx +55 -0
- package/src/components/ChartTable/ChartTableModal.tsx +135 -0
- package/src/components/ChartTable/ChartToolbar.tsx +50 -0
- package/src/components/ChartTable/chartToolbarI18n.ts +55 -0
- package/src/components/ChartTable/index.ts +23 -0
- package/src/components/LineChart/DataTable.tsx +3 -3
- package/src/components/LineChart/LineChart.tsx +1 -1
- package/src/index.ts +1 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
act,
|
|
5
|
+
render,
|
|
6
|
+
screen,
|
|
7
|
+
fireEvent,
|
|
8
|
+
waitFor,
|
|
9
|
+
} from '@testing-library/react';
|
|
10
|
+
import { DropdownMenuItem } from 'react-magma-dom';
|
|
11
|
+
import {
|
|
12
|
+
FullscreenExitIcon,
|
|
13
|
+
FullscreenIcon,
|
|
14
|
+
MoreVertIcon,
|
|
15
|
+
TableChartIcon,
|
|
16
|
+
} from 'react-magma-icons';
|
|
17
|
+
|
|
18
|
+
import { ChartDataTable } from './ChartDataTable';
|
|
19
|
+
import { ChartFullscreenButton } from './ChartFullscreenButton';
|
|
20
|
+
import { ChartMoreOptionsButton } from './ChartMoreOptionsButton';
|
|
21
|
+
import { ChartTableButton } from './ChartTableButton';
|
|
22
|
+
import { ChartTableModal } from './ChartTableModal';
|
|
23
|
+
import { ChartToolbar } from './ChartToolbar';
|
|
24
|
+
|
|
25
|
+
const dataSet = [
|
|
26
|
+
{ group: 'High performance', value: 50 },
|
|
27
|
+
{ group: 'Average performance', value: 30 },
|
|
28
|
+
{ group: 'Poor performance', value: 15 },
|
|
29
|
+
{ group: 'Not attempted', value: 5 },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// ChartDataTable
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
describe('ChartDataTable', () => {
|
|
36
|
+
it('derives column headers from dataset keys when no columns prop is given', () => {
|
|
37
|
+
render(<ChartDataTable dataSet={dataSet} />);
|
|
38
|
+
|
|
39
|
+
expect(
|
|
40
|
+
screen.getByRole('columnheader', { name: 'Group' })
|
|
41
|
+
).toBeInTheDocument();
|
|
42
|
+
expect(
|
|
43
|
+
screen.getByRole('columnheader', { name: 'Value' })
|
|
44
|
+
).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('renders all data rows', () => {
|
|
48
|
+
render(<ChartDataTable dataSet={dataSet} />);
|
|
49
|
+
|
|
50
|
+
expect(
|
|
51
|
+
screen.getByRole('cell', { name: 'High performance' })
|
|
52
|
+
).toBeInTheDocument();
|
|
53
|
+
expect(screen.getByRole('cell', { name: '50' })).toBeInTheDocument();
|
|
54
|
+
expect(
|
|
55
|
+
screen.getByRole('cell', { name: 'Not attempted' })
|
|
56
|
+
).toBeInTheDocument();
|
|
57
|
+
expect(screen.getByRole('cell', { name: '5' })).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('accepts custom column definitions', () => {
|
|
61
|
+
const columns = [
|
|
62
|
+
{ header: 'Category', key: 'group' },
|
|
63
|
+
{ header: 'Count', key: 'value' },
|
|
64
|
+
];
|
|
65
|
+
render(<ChartDataTable columns={columns} dataSet={dataSet} />);
|
|
66
|
+
|
|
67
|
+
expect(
|
|
68
|
+
screen.getByRole('columnheader', { name: 'Category' })
|
|
69
|
+
).toBeInTheDocument();
|
|
70
|
+
expect(
|
|
71
|
+
screen.getByRole('columnheader', { name: 'Count' })
|
|
72
|
+
).toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('renders with isInverse without error', () => {
|
|
76
|
+
render(<ChartDataTable dataSet={dataSet} isInverse />);
|
|
77
|
+
|
|
78
|
+
expect(screen.getByRole('table')).toBeInTheDocument();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// ChartTableModal
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
describe('ChartTableModal', () => {
|
|
86
|
+
it('renders a dialog with semantic heading when open', () => {
|
|
87
|
+
render(
|
|
88
|
+
<ChartTableModal
|
|
89
|
+
dataSet={dataSet}
|
|
90
|
+
isOpen
|
|
91
|
+
onClose={jest.fn()}
|
|
92
|
+
title="Overall Performance"
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
|
97
|
+
expect(
|
|
98
|
+
screen.getByRole('heading', {
|
|
99
|
+
level: 2,
|
|
100
|
+
name: 'Tabular representation Overall Performance',
|
|
101
|
+
})
|
|
102
|
+
).toBeInTheDocument();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('respects custom headerLevel', () => {
|
|
106
|
+
render(
|
|
107
|
+
<ChartTableModal
|
|
108
|
+
dataSet={dataSet}
|
|
109
|
+
headerLevel={1}
|
|
110
|
+
isOpen
|
|
111
|
+
onClose={jest.fn()}
|
|
112
|
+
title="Test"
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('respects custom headerLabel', () => {
|
|
120
|
+
render(
|
|
121
|
+
<ChartTableModal
|
|
122
|
+
dataSet={dataSet}
|
|
123
|
+
headerLabel="Data table"
|
|
124
|
+
isOpen
|
|
125
|
+
onClose={jest.fn()}
|
|
126
|
+
title="My Chart"
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
expect(
|
|
131
|
+
screen.getByRole('heading', { name: 'Data table My Chart' })
|
|
132
|
+
).toBeInTheDocument();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('does not render dialog when closed', () => {
|
|
136
|
+
render(
|
|
137
|
+
<ChartTableModal
|
|
138
|
+
dataSet={dataSet}
|
|
139
|
+
isOpen={false}
|
|
140
|
+
onClose={jest.fn()}
|
|
141
|
+
title="Test"
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('renders the data table inside the modal', () => {
|
|
149
|
+
render(
|
|
150
|
+
<ChartTableModal
|
|
151
|
+
dataSet={dataSet}
|
|
152
|
+
isOpen
|
|
153
|
+
onClose={jest.fn()}
|
|
154
|
+
title="Test"
|
|
155
|
+
/>
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(
|
|
159
|
+
screen.getByRole('columnheader', { name: 'Group' })
|
|
160
|
+
).toBeInTheDocument();
|
|
161
|
+
expect(
|
|
162
|
+
screen.getByRole('cell', { name: 'High performance' })
|
|
163
|
+
).toBeInTheDocument();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('calls onClose when close button is activated', async () => {
|
|
167
|
+
jest.useFakeTimers();
|
|
168
|
+
const onClose = jest.fn();
|
|
169
|
+
render(
|
|
170
|
+
<ChartTableModal
|
|
171
|
+
dataSet={dataSet}
|
|
172
|
+
isOpen
|
|
173
|
+
onClose={onClose}
|
|
174
|
+
title="Test"
|
|
175
|
+
/>
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
fireEvent.click(screen.getByTestId('modal-closebtn'));
|
|
179
|
+
|
|
180
|
+
await act(async () => {
|
|
181
|
+
jest.runAllTimers();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
185
|
+
jest.useRealTimers();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('renders with isInverse without error', () => {
|
|
189
|
+
render(
|
|
190
|
+
<ChartTableModal
|
|
191
|
+
dataSet={dataSet}
|
|
192
|
+
isInverse
|
|
193
|
+
isOpen
|
|
194
|
+
onClose={jest.fn()}
|
|
195
|
+
title="Inverse Modal"
|
|
196
|
+
/>
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// ChartTableButton
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
describe('ChartTableButton', () => {
|
|
207
|
+
const icon = <TableChartIcon />;
|
|
208
|
+
|
|
209
|
+
it('renders with aria-haspopup="dialog"', () => {
|
|
210
|
+
render(
|
|
211
|
+
<ChartTableButton
|
|
212
|
+
ariaLabel="Overall Performance"
|
|
213
|
+
icon={icon}
|
|
214
|
+
isTableOpen={false}
|
|
215
|
+
onClick={jest.fn()}
|
|
216
|
+
/>
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const button = screen.getByRole('button', { name: 'Overall Performance' });
|
|
220
|
+
expect(button).toHaveAttribute('aria-haspopup', 'dialog');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('sets aria-expanded to false when modal is closed', () => {
|
|
224
|
+
render(
|
|
225
|
+
<ChartTableButton
|
|
226
|
+
ariaLabel="Overall Performance"
|
|
227
|
+
icon={icon}
|
|
228
|
+
isTableOpen={false}
|
|
229
|
+
onClick={jest.fn()}
|
|
230
|
+
/>
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
expect(
|
|
234
|
+
screen.getByRole('button', { name: 'Overall Performance' })
|
|
235
|
+
).toHaveAttribute('aria-expanded', 'false');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('sets aria-expanded to true when modal is open', () => {
|
|
239
|
+
render(
|
|
240
|
+
<ChartTableButton
|
|
241
|
+
ariaLabel="Overall Performance"
|
|
242
|
+
icon={icon}
|
|
243
|
+
isTableOpen
|
|
244
|
+
onClick={jest.fn()}
|
|
245
|
+
/>
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
expect(
|
|
249
|
+
screen.getByRole('button', { name: 'Overall Performance' })
|
|
250
|
+
).toHaveAttribute('aria-expanded', 'true');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('calls onClick when activated', () => {
|
|
254
|
+
const onClick = jest.fn();
|
|
255
|
+
render(
|
|
256
|
+
<ChartTableButton
|
|
257
|
+
ariaLabel="Overall Performance"
|
|
258
|
+
icon={icon}
|
|
259
|
+
isTableOpen={false}
|
|
260
|
+
onClick={onClick}
|
|
261
|
+
/>
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
fireEvent.click(
|
|
265
|
+
screen.getByRole('button', { name: 'Overall Performance' })
|
|
266
|
+
);
|
|
267
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('renders with custom tooltip content on hover', async () => {
|
|
271
|
+
render(
|
|
272
|
+
<ChartTableButton
|
|
273
|
+
ariaLabel="Overall Performance"
|
|
274
|
+
icon={icon}
|
|
275
|
+
isTableOpen={false}
|
|
276
|
+
onClick={jest.fn()}
|
|
277
|
+
tooltipContent="View data as table"
|
|
278
|
+
/>
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const button = screen.getByRole('button', { name: 'Overall Performance' });
|
|
282
|
+
fireEvent.mouseEnter(button);
|
|
283
|
+
|
|
284
|
+
await waitFor(() => {
|
|
285
|
+
expect(screen.getByText('View data as table')).toBeInTheDocument();
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// ChartFullscreenButton
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
describe('ChartFullscreenButton', () => {
|
|
294
|
+
it('does NOT have aria-haspopup', () => {
|
|
295
|
+
render(
|
|
296
|
+
<ChartFullscreenButton
|
|
297
|
+
ariaLabel="View chart in full screen"
|
|
298
|
+
icon={<FullscreenIcon />}
|
|
299
|
+
isFullscreen={false}
|
|
300
|
+
onClick={jest.fn()}
|
|
301
|
+
/>
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const button = screen.getByRole('button', {
|
|
305
|
+
name: 'View chart in full screen',
|
|
306
|
+
});
|
|
307
|
+
expect(button).not.toHaveAttribute('aria-haspopup');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('calls onClick when activated', () => {
|
|
311
|
+
const onClick = jest.fn();
|
|
312
|
+
render(
|
|
313
|
+
<ChartFullscreenButton
|
|
314
|
+
ariaLabel="View chart in full screen"
|
|
315
|
+
icon={<FullscreenIcon />}
|
|
316
|
+
isFullscreen={false}
|
|
317
|
+
onClick={onClick}
|
|
318
|
+
/>
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
fireEvent.click(
|
|
322
|
+
screen.getByRole('button', { name: 'View chart in full screen' })
|
|
323
|
+
);
|
|
324
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('shows "Make full screen" tooltip by default', async () => {
|
|
328
|
+
render(
|
|
329
|
+
<ChartFullscreenButton
|
|
330
|
+
ariaLabel="Fullscreen"
|
|
331
|
+
icon={<FullscreenIcon />}
|
|
332
|
+
isFullscreen={false}
|
|
333
|
+
onClick={jest.fn()}
|
|
334
|
+
/>
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
fireEvent.mouseEnter(screen.getByRole('button', { name: 'Fullscreen' }));
|
|
338
|
+
await waitFor(() => {
|
|
339
|
+
expect(screen.getByText('Make full screen')).toBeInTheDocument();
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('shows "Exit full screen" tooltip when in fullscreen', async () => {
|
|
344
|
+
render(
|
|
345
|
+
<ChartFullscreenButton
|
|
346
|
+
ariaLabel="Fullscreen"
|
|
347
|
+
icon={<FullscreenIcon />}
|
|
348
|
+
exitIcon={<FullscreenExitIcon />}
|
|
349
|
+
isFullscreen
|
|
350
|
+
onClick={jest.fn()}
|
|
351
|
+
/>
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
fireEvent.mouseEnter(screen.getByRole('button', { name: 'Fullscreen' }));
|
|
355
|
+
await waitFor(() => {
|
|
356
|
+
expect(screen.getByText('Exit full screen')).toBeInTheDocument();
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
// ChartMoreOptionsButton
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
describe('ChartMoreOptionsButton', () => {
|
|
365
|
+
it('renders with default "More options" label', () => {
|
|
366
|
+
render(
|
|
367
|
+
<ChartMoreOptionsButton icon={<MoreVertIcon />}>
|
|
368
|
+
<DropdownMenuItem>Download</DropdownMenuItem>
|
|
369
|
+
</ChartMoreOptionsButton>
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
expect(
|
|
373
|
+
screen.getByRole('button', { name: 'More options' })
|
|
374
|
+
).toBeInTheDocument();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('renders with custom aria-label', () => {
|
|
378
|
+
render(
|
|
379
|
+
<ChartMoreOptionsButton ariaLabel="Chart actions" icon={<MoreVertIcon />}>
|
|
380
|
+
<DropdownMenuItem>Download</DropdownMenuItem>
|
|
381
|
+
</ChartMoreOptionsButton>
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
expect(
|
|
385
|
+
screen.getByRole('button', { name: 'Chart actions' })
|
|
386
|
+
).toBeInTheDocument();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('opens dropdown menu on click', () => {
|
|
390
|
+
render(
|
|
391
|
+
<ChartMoreOptionsButton icon={<MoreVertIcon />}>
|
|
392
|
+
<DropdownMenuItem>Download as CSV</DropdownMenuItem>
|
|
393
|
+
</ChartMoreOptionsButton>
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
fireEvent.click(screen.getByRole('button', { name: 'More options' }));
|
|
397
|
+
expect(screen.getByText('Download as CSV')).toBeVisible();
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
// ChartToolbar
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
describe('ChartToolbar', () => {
|
|
405
|
+
it('renders the title as a heading', () => {
|
|
406
|
+
render(
|
|
407
|
+
<ChartToolbar title="Overall Performance">
|
|
408
|
+
<button>Action</button>
|
|
409
|
+
</ChartToolbar>
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
expect(
|
|
413
|
+
screen.getByRole('heading', { level: 3, name: 'Overall Performance' })
|
|
414
|
+
).toBeInTheDocument();
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('renders children action buttons', () => {
|
|
418
|
+
render(
|
|
419
|
+
<ChartToolbar title="Test">
|
|
420
|
+
<button>Show as table</button>
|
|
421
|
+
<button>Full screen</button>
|
|
422
|
+
</ChartToolbar>
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
expect(
|
|
426
|
+
screen.getByRole('button', { name: 'Show as table' })
|
|
427
|
+
).toBeInTheDocument();
|
|
428
|
+
expect(
|
|
429
|
+
screen.getByRole('button', { name: 'Full screen' })
|
|
430
|
+
).toBeInTheDocument();
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('respects custom heading level', () => {
|
|
434
|
+
render(
|
|
435
|
+
<ChartToolbar title="Test" headingLevel={2}>
|
|
436
|
+
<button>Action</button>
|
|
437
|
+
</ChartToolbar>
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
expect(
|
|
441
|
+
screen.getByRole('heading', { level: 2, name: 'Test' })
|
|
442
|
+
).toBeInTheDocument();
|
|
443
|
+
});
|
|
444
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { ButtonVariant, IconButton, Tooltip } from 'react-magma-dom';
|
|
4
|
+
|
|
5
|
+
import { useChartToolbarI18n } from './chartToolbarI18n';
|
|
6
|
+
|
|
7
|
+
export interface ChartTableButtonProps {
|
|
8
|
+
/** Accessible label for the button – should describe the chart, e.g. "Overall Performance" */
|
|
9
|
+
ariaLabel: string;
|
|
10
|
+
/** Icon element rendered inside the button */
|
|
11
|
+
icon: React.ReactElement;
|
|
12
|
+
/**
|
|
13
|
+
* If true, the button uses inverse (dark) styling.
|
|
14
|
+
* @default false
|
|
15
|
+
*/
|
|
16
|
+
isInverse?: boolean;
|
|
17
|
+
/** Whether the associated modal is currently open */
|
|
18
|
+
isTableOpen: boolean;
|
|
19
|
+
/** Click handler – should open the modal */
|
|
20
|
+
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
21
|
+
/** Optional ref forwarded to the underlying IconButton */
|
|
22
|
+
buttonRef?: React.Ref<HTMLButtonElement>;
|
|
23
|
+
/**
|
|
24
|
+
* Tooltip text shown on hover.
|
|
25
|
+
* @default "Show as table" (i18n overridable)
|
|
26
|
+
*/
|
|
27
|
+
tooltipContent?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function ChartTableButton({
|
|
31
|
+
ariaLabel,
|
|
32
|
+
buttonRef,
|
|
33
|
+
icon,
|
|
34
|
+
isInverse,
|
|
35
|
+
isTableOpen,
|
|
36
|
+
onClick,
|
|
37
|
+
tooltipContent,
|
|
38
|
+
}: ChartTableButtonProps) {
|
|
39
|
+
const t = useChartToolbarI18n();
|
|
40
|
+
const resolvedTooltip = tooltipContent ?? t.showAsTableTooltip;
|
|
41
|
+
return (
|
|
42
|
+
<Tooltip content={resolvedTooltip} isInverse={isInverse}>
|
|
43
|
+
<IconButton
|
|
44
|
+
aria-expanded={isTableOpen}
|
|
45
|
+
aria-haspopup="dialog"
|
|
46
|
+
aria-label={ariaLabel}
|
|
47
|
+
icon={icon}
|
|
48
|
+
isInverse={isInverse}
|
|
49
|
+
onClick={onClick}
|
|
50
|
+
ref={buttonRef}
|
|
51
|
+
variant={ButtonVariant.link}
|
|
52
|
+
/>
|
|
53
|
+
</Tooltip>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import styled from '@emotion/styled';
|
|
4
|
+
import { Button, Modal, ModalSize, ThemeContext } from 'react-magma-dom';
|
|
5
|
+
|
|
6
|
+
import { ChartDataTable, ChartDataTableColumn } from './ChartDataTable';
|
|
7
|
+
import { useChartToolbarI18n } from './chartToolbarI18n';
|
|
8
|
+
|
|
9
|
+
export interface ChartTableModalProps {
|
|
10
|
+
/** The chart's data passed to ChartDataTable */
|
|
11
|
+
dataSet: Array<Record<string, React.ReactNode>>;
|
|
12
|
+
/** Column definitions forwarded to ChartDataTable */
|
|
13
|
+
columns?: ChartDataTableColumn[];
|
|
14
|
+
/**
|
|
15
|
+
* DOM element to portal the modal into. Defaults to `document.body`.
|
|
16
|
+
* When the chart is in fullscreen, pass the fullscreen element so the
|
|
17
|
+
* modal renders inside it and is visible to the browser.
|
|
18
|
+
*/
|
|
19
|
+
portalContainer?: HTMLElement | null;
|
|
20
|
+
/**
|
|
21
|
+
* If true, the modal uses inverse (dark) styling.
|
|
22
|
+
* @default false
|
|
23
|
+
*/
|
|
24
|
+
isInverse?: boolean;
|
|
25
|
+
/** Whether the modal is open */
|
|
26
|
+
isOpen: boolean;
|
|
27
|
+
/** Called when the modal requests to close */
|
|
28
|
+
onClose: () => void;
|
|
29
|
+
/** Called when the "Download as CSV" footer button is clicked */
|
|
30
|
+
onDownloadCsv?: () => void;
|
|
31
|
+
/** Chart title – displayed as a second line in the modal heading */
|
|
32
|
+
title: string;
|
|
33
|
+
/**
|
|
34
|
+
* Heading level for the modal header (1–6).
|
|
35
|
+
* @default 2
|
|
36
|
+
*/
|
|
37
|
+
headerLevel?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
38
|
+
/**
|
|
39
|
+
* First line of the modal heading.
|
|
40
|
+
* @default "Tabular representation" (i18n overridable)
|
|
41
|
+
*/
|
|
42
|
+
headerLabel?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Magma Modal size.
|
|
45
|
+
* @default ModalSize.large
|
|
46
|
+
*/
|
|
47
|
+
size?: ModalSize;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const ModalFooter = styled.div<{ theme: any }>`
|
|
51
|
+
display: flex;
|
|
52
|
+
justify-content: flex-end;
|
|
53
|
+
padding-top: ${props => props.theme.spaceScale.spacing05};
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const HeaderLabel = styled.span<{ isInverse?: boolean; theme: any }>`
|
|
57
|
+
display: block;
|
|
58
|
+
font-size: ${props => props.theme.typeScale.size02.fontSize};
|
|
59
|
+
font-weight: normal;
|
|
60
|
+
color: ${props =>
|
|
61
|
+
props.isInverse
|
|
62
|
+
? props.theme.colors.neutral100
|
|
63
|
+
: props.theme.colors.neutral500};
|
|
64
|
+
line-height: ${props => props.theme.typeScale.size02.lineHeight};
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
const containerStyle: React.CSSProperties = {
|
|
68
|
+
display: 'flex',
|
|
69
|
+
alignItems: 'center',
|
|
70
|
+
justifyContent: 'center',
|
|
71
|
+
};
|
|
72
|
+
const contentStyle: React.CSSProperties = {
|
|
73
|
+
width: '100%',
|
|
74
|
+
margin: 0,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const HeaderTitle = styled.span`
|
|
78
|
+
display: block;
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
export function ChartTableModal({
|
|
82
|
+
columns,
|
|
83
|
+
portalContainer,
|
|
84
|
+
dataSet,
|
|
85
|
+
headerLabel,
|
|
86
|
+
headerLevel = 2,
|
|
87
|
+
isInverse,
|
|
88
|
+
isOpen,
|
|
89
|
+
onClose,
|
|
90
|
+
onDownloadCsv,
|
|
91
|
+
size = ModalSize.large,
|
|
92
|
+
title,
|
|
93
|
+
}: ChartTableModalProps) {
|
|
94
|
+
const t = useChartToolbarI18n();
|
|
95
|
+
const theme = React.useContext(ThemeContext);
|
|
96
|
+
const resolvedHeaderLabel = headerLabel ?? t.tabularRepresentationLabel;
|
|
97
|
+
const header = React.useMemo(
|
|
98
|
+
() => (
|
|
99
|
+
<span>
|
|
100
|
+
<HeaderLabel isInverse={isInverse} theme={theme}>
|
|
101
|
+
{resolvedHeaderLabel}
|
|
102
|
+
</HeaderLabel>
|
|
103
|
+
<HeaderTitle>{title}</HeaderTitle>
|
|
104
|
+
</span>
|
|
105
|
+
),
|
|
106
|
+
[resolvedHeaderLabel, title, isInverse, theme]
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<Modal
|
|
111
|
+
portalContainer={portalContainer}
|
|
112
|
+
containerStyle={portalContainer ? containerStyle : undefined}
|
|
113
|
+
header={header}
|
|
114
|
+
headerLevel={headerLevel}
|
|
115
|
+
isInverse={isInverse}
|
|
116
|
+
isOpen={isOpen}
|
|
117
|
+
onClose={onClose}
|
|
118
|
+
size={size}
|
|
119
|
+
style={portalContainer ? contentStyle : undefined}
|
|
120
|
+
>
|
|
121
|
+
<ChartDataTable
|
|
122
|
+
columns={columns}
|
|
123
|
+
dataSet={dataSet}
|
|
124
|
+
isInverse={isInverse}
|
|
125
|
+
/>
|
|
126
|
+
{onDownloadCsv && (
|
|
127
|
+
<ModalFooter theme={theme}>
|
|
128
|
+
<Button isInverse={isInverse} onClick={onDownloadCsv}>
|
|
129
|
+
{t.downloadAsCsv}
|
|
130
|
+
</Button>
|
|
131
|
+
</ModalFooter>
|
|
132
|
+
)}
|
|
133
|
+
</Modal>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import styled from '@emotion/styled';
|
|
4
|
+
import { Heading, ThemeContext, TypographyVisualStyle } from 'react-magma-dom';
|
|
5
|
+
|
|
6
|
+
export interface ChartToolbarProps {
|
|
7
|
+
/** Toolbar action buttons (ChartTableButton, ChartFullscreenButton, ChartMoreOptionsButton, etc.) */
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
/**
|
|
10
|
+
* Heading level for the chart title.
|
|
11
|
+
* @default 3
|
|
12
|
+
*/
|
|
13
|
+
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
14
|
+
/**
|
|
15
|
+
* Visual style for the heading.
|
|
16
|
+
* @default TypographyVisualStyle.headingSmall
|
|
17
|
+
*/
|
|
18
|
+
headingVisualStyle?: TypographyVisualStyle;
|
|
19
|
+
/** Chart title text */
|
|
20
|
+
title: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ToolbarWrapper = styled.div`
|
|
24
|
+
align-items: center;
|
|
25
|
+
display: flex;
|
|
26
|
+
justify-content: space-between;
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
const ActionsWrapper = styled.div<{ theme: any }>`
|
|
30
|
+
align-items: center;
|
|
31
|
+
display: flex;
|
|
32
|
+
gap: ${props => props.theme.spaceScale.spacing02};
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
export function ChartToolbar({
|
|
36
|
+
children,
|
|
37
|
+
headingLevel = 3,
|
|
38
|
+
headingVisualStyle = TypographyVisualStyle.headingSmall,
|
|
39
|
+
title,
|
|
40
|
+
}: ChartToolbarProps) {
|
|
41
|
+
const theme = React.useContext(ThemeContext);
|
|
42
|
+
return (
|
|
43
|
+
<ToolbarWrapper>
|
|
44
|
+
<Heading level={headingLevel} noMargins visualStyle={headingVisualStyle}>
|
|
45
|
+
{title}
|
|
46
|
+
</Heading>
|
|
47
|
+
<ActionsWrapper theme={theme}>{children}</ActionsWrapper>
|
|
48
|
+
</ToolbarWrapper>
|
|
49
|
+
);
|
|
50
|
+
}
|