@react-magma/charts 14.0.0-next.2 → 14.0.0-next.4

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.
Files changed (58) hide show
  1. package/dist/charts.js +851 -69
  2. package/dist/charts.js.map +1 -1
  3. package/dist/charts.modern.module.js +847 -72
  4. package/dist/charts.modern.module.js.map +1 -1
  5. package/dist/charts.umd.js +3088 -680
  6. package/dist/charts.umd.js.map +1 -1
  7. package/dist/components/CarbonChart/CarbonChart.d.ts +46 -0
  8. package/dist/components/CarbonChart/CarbonChartBar.stories.d.ts +27 -9
  9. package/dist/components/ChartTable/ChartDataTable.d.ts +19 -0
  10. package/dist/components/ChartTable/ChartFullscreenButton.d.ts +26 -0
  11. package/dist/components/ChartTable/ChartMoreOptionsButton.d.ts +18 -0
  12. package/dist/components/ChartTable/ChartTable.stories.d.ts +116 -0
  13. package/dist/components/ChartTable/ChartTableButton.d.ts +24 -0
  14. package/dist/components/ChartTable/ChartTableModal.d.ts +44 -0
  15. package/dist/components/ChartTable/ChartToolbar.d.ts +19 -0
  16. package/dist/components/ChartTable/chartToolbarI18n.d.ts +17 -0
  17. package/dist/components/ChartTable/index.d.ts +14 -0
  18. package/dist/components/LineChart/DataTable.d.ts +1 -1
  19. package/dist/hooks/useCarbonModalFocusManagement.d.ts +2 -0
  20. package/dist/index.d.ts +1 -0
  21. package/package.json +5 -5
  22. package/src/components/CarbonChart/CarbonChart.test.js +645 -2
  23. package/src/components/CarbonChart/CarbonChart.tsx +950 -59
  24. package/src/components/CarbonChart/CarbonChartArea.stories.tsx +1 -1
  25. package/src/components/CarbonChart/CarbonChartAreaStacked.stories.tsx +1 -1
  26. package/src/components/CarbonChart/CarbonChartBar.stories.tsx +9 -2
  27. package/src/components/CarbonChart/CarbonChartBarFloating.stories.tsx +1 -1
  28. package/src/components/CarbonChart/CarbonChartBarGrouped.stories.tsx +1 -1
  29. package/src/components/CarbonChart/CarbonChartBarStacked.stories.tsx +1 -1
  30. package/src/components/CarbonChart/CarbonChartBoxplot.stories.tsx +1 -1
  31. package/src/components/CarbonChart/CarbonChartBubble.stories.tsx +1 -1
  32. package/src/components/CarbonChart/CarbonChartBullet.stories.tsx +1 -1
  33. package/src/components/CarbonChart/CarbonChartCombo.stories.tsx +1 -1
  34. package/src/components/CarbonChart/CarbonChartDonut.stories.tsx +3 -1
  35. package/src/components/CarbonChart/CarbonChartGauge.stories.tsx +1 -1
  36. package/src/components/CarbonChart/CarbonChartHistogram.stories.tsx +1 -1
  37. package/src/components/CarbonChart/CarbonChartLine.stories.tsx +1 -1
  38. package/src/components/CarbonChart/CarbonChartLollipop.stories.tsx +1 -1
  39. package/src/components/CarbonChart/CarbonChartMeter.stories.tsx +1 -1
  40. package/src/components/CarbonChart/CarbonChartPie.stories.tsx +1 -1
  41. package/src/components/CarbonChart/CarbonChartRadar.stories.tsx +1 -1
  42. package/src/components/CarbonChart/CarbonChartScatter.stories.tsx +1 -1
  43. package/src/components/CarbonChart/CarbonChartSparkline.stories.tsx +1 -1
  44. package/src/components/CarbonChart/CarbonChartStep.stories.tsx +1 -1
  45. package/src/components/ChartTable/ChartDataTable.tsx +72 -0
  46. package/src/components/ChartTable/ChartFullscreenButton.tsx +59 -0
  47. package/src/components/ChartTable/ChartMoreOptionsButton.tsx +48 -0
  48. package/src/components/ChartTable/ChartTable.stories.tsx +152 -0
  49. package/src/components/ChartTable/ChartTable.test.js +444 -0
  50. package/src/components/ChartTable/ChartTableButton.tsx +55 -0
  51. package/src/components/ChartTable/ChartTableModal.tsx +135 -0
  52. package/src/components/ChartTable/ChartToolbar.tsx +50 -0
  53. package/src/components/ChartTable/chartToolbarI18n.ts +59 -0
  54. package/src/components/ChartTable/index.ts +23 -0
  55. package/src/components/LineChart/DataTable.tsx +3 -3
  56. package/src/components/LineChart/LineChart.tsx +1 -1
  57. package/src/hooks/useCarbonModalFocusManagement.ts +173 -0
  58. package/src/index.ts +1 -0
@@ -0,0 +1,59 @@
1
+ import * as React from 'react';
2
+
3
+ import { I18nContext } from 'react-magma-dom';
4
+
5
+ export interface ChartToolbarI18n {
6
+ defaultTitle: string;
7
+ downloadAsCsv: string;
8
+ downloadAsJpg: string;
9
+ downloadAsPng: string;
10
+ exitFullScreen: string;
11
+ legendInstructions: string;
12
+ makeFullScreen: string;
13
+ moreOptionsAriaLabel: string;
14
+ showAsTableTooltip: string;
15
+ tabularRepresentationLabel: string;
16
+ }
17
+
18
+ const defaults: ChartToolbarI18n = {
19
+ defaultTitle: 'Chart',
20
+ downloadAsCsv: 'Download as CSV',
21
+ downloadAsJpg: 'Download as JPG',
22
+ downloadAsPng: 'Download as PNG',
23
+ exitFullScreen: 'Exit full screen',
24
+ legendInstructions: 'Checking these checkboxes will update the chart.',
25
+ makeFullScreen: 'Make full screen',
26
+ moreOptionsAriaLabel: 'More options',
27
+ showAsTableTooltip: 'Show as table',
28
+ tabularRepresentationLabel: 'Tabular representation',
29
+ };
30
+
31
+ /**
32
+ * Reads chart toolbar i18n strings from the I18nContext if available,
33
+ * falling back to English defaults.
34
+ */
35
+ // eslint-disable-next-line complexity
36
+ export function useChartToolbarI18n(): ChartToolbarI18n {
37
+ const i18n = React.useContext(I18nContext);
38
+ const toolbar = (i18n.charts as { toolbar?: Partial<ChartToolbarI18n> })
39
+ ?.toolbar;
40
+
41
+ if (!toolbar) return defaults;
42
+
43
+ return {
44
+ defaultTitle: toolbar.defaultTitle ?? defaults.defaultTitle,
45
+ downloadAsCsv: toolbar.downloadAsCsv ?? defaults.downloadAsCsv,
46
+ downloadAsJpg: toolbar.downloadAsJpg ?? defaults.downloadAsJpg,
47
+ downloadAsPng: toolbar.downloadAsPng ?? defaults.downloadAsPng,
48
+ exitFullScreen: toolbar.exitFullScreen ?? defaults.exitFullScreen,
49
+ legendInstructions:
50
+ toolbar.legendInstructions ?? defaults.legendInstructions,
51
+ makeFullScreen: toolbar.makeFullScreen ?? defaults.makeFullScreen,
52
+ moreOptionsAriaLabel:
53
+ toolbar.moreOptionsAriaLabel ?? defaults.moreOptionsAriaLabel,
54
+ showAsTableTooltip:
55
+ toolbar.showAsTableTooltip ?? defaults.showAsTableTooltip,
56
+ tabularRepresentationLabel:
57
+ toolbar.tabularRepresentationLabel ?? defaults.tabularRepresentationLabel,
58
+ };
59
+ }
@@ -0,0 +1,23 @@
1
+ export { ChartDataTable } from './ChartDataTable';
2
+ export type {
3
+ ChartDataTableProps,
4
+ ChartDataTableColumn,
5
+ } from './ChartDataTable';
6
+
7
+ export { ChartFullscreenButton } from './ChartFullscreenButton';
8
+ export type { ChartFullscreenButtonProps } from './ChartFullscreenButton';
9
+
10
+ export { ChartMoreOptionsButton } from './ChartMoreOptionsButton';
11
+ export type { ChartMoreOptionsButtonProps } from './ChartMoreOptionsButton';
12
+
13
+ export { ChartTableButton } from './ChartTableButton';
14
+ export type { ChartTableButtonProps } from './ChartTableButton';
15
+
16
+ export { ChartTableModal } from './ChartTableModal';
17
+ export type { ChartTableModalProps } from './ChartTableModal';
18
+
19
+ export { ChartToolbar } from './ChartToolbar';
20
+ export type { ChartToolbarProps } from './ChartToolbar';
21
+
22
+ export { useChartToolbarI18n } from './chartToolbarI18n';
23
+ export type { ChartToolbarI18n } from './chartToolbarI18n';
@@ -15,7 +15,7 @@ export interface DataTableProps {
15
15
  data?: any;
16
16
  }
17
17
 
18
- export const DataTable = props => {
18
+ export const DataTable = (props: DataTableProps) => {
19
19
  const { data } = props;
20
20
 
21
21
  return (
@@ -32,8 +32,8 @@ export const DataTable = props => {
32
32
  </TableRow>
33
33
  </TableHead>
34
34
  <TableBody>
35
- {data.map((dataset, i) => (
36
- <TableRow key={i}>
35
+ {data.map((dataset: any, i: number) => (
36
+ <TableRow key={dataset.label ?? `row-${i}`}>
37
37
  <TableHeaderCell
38
38
  scope={TableHeaderCellScope.row}
39
39
  style={{
@@ -653,7 +653,7 @@ export function LineChart<T>(props: LineChartProps<T>) {
653
653
  color={theme.iterableColors[i]}
654
654
  dataIndex={i}
655
655
  isHidden={hiddenData.includes(i)}
656
- key={i}
656
+ key={name}
657
657
  name={name}
658
658
  onClick={handleLegendClick}
659
659
  onKeyDown={i === 0 ? handleFirstLegendButtonKeydown : undefined}
@@ -0,0 +1,173 @@
1
+ import * as React from 'react';
2
+
3
+ const FOCUSABLE_SELECTOR =
4
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
5
+
6
+ function getFocusableElements(container: HTMLElement): HTMLElement[] {
7
+ return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter(
8
+ (el): el is HTMLElement => {
9
+ const style = window.getComputedStyle(el);
10
+ return (
11
+ style.display !== 'none' &&
12
+ style.visibility !== 'hidden' &&
13
+ !el.hasAttribute('disabled')
14
+ );
15
+ }
16
+ );
17
+ }
18
+
19
+ function findVisibleModal(wrapper: HTMLElement): HTMLElement | null {
20
+ const modal = wrapper.querySelector<HTMLElement>('.cds--modal');
21
+ if (!modal) return null;
22
+
23
+ const isVisible =
24
+ modal.getAttribute('aria-modal') === 'true' ||
25
+ modal.style.visibility === 'visible' ||
26
+ modal.classList.contains('is-visible');
27
+
28
+ return isVisible ? modal : null;
29
+ }
30
+
31
+ export function useCarbonModalFocusManagement(
32
+ wrapperRef: React.RefObject<HTMLDivElement>
33
+ ): void {
34
+ const previouslyFocusedElement = React.useRef<Element | null>(null);
35
+ const keydownHandler = React.useRef<((e: KeyboardEvent) => void) | null>(
36
+ null
37
+ );
38
+ const focusinHandler = React.useRef<((e: FocusEvent) => void) | null>(null);
39
+ const currentModal = React.useRef<HTMLElement | null>(null);
40
+
41
+ React.useEffect(() => {
42
+ const wrapper = wrapperRef.current;
43
+ if (!wrapper) return;
44
+
45
+ function focusModalCloseButton(modal: HTMLElement) {
46
+ const closeButton = modal.querySelector<HTMLElement>('.cds--modal-close');
47
+ if (closeButton) {
48
+ closeButton.focus();
49
+ } else {
50
+ const focusable = getFocusableElements(modal);
51
+ if (focusable.length > 0) {
52
+ focusable[0].focus();
53
+ }
54
+ }
55
+ }
56
+
57
+ function handleModalOpen(modal: HTMLElement) {
58
+ currentModal.current = modal;
59
+ previouslyFocusedElement.current = document.activeElement;
60
+
61
+ // Permanent guard: redirect focus back into modal whenever it escapes
62
+ // (e.g. Carbon's overflow menu returning focus to its trigger).
63
+ focusinHandler.current = (event: FocusEvent) => {
64
+ const target = event.target as HTMLElement;
65
+ if (!modal.contains(target)) {
66
+ setTimeout(() => {
67
+ if (currentModal.current === modal) {
68
+ focusModalCloseButton(modal);
69
+ }
70
+ }, 0);
71
+ }
72
+ };
73
+ document.addEventListener('focusin', focusinHandler.current);
74
+
75
+ let pollAttempts = 0;
76
+ const pollAndFocus = () => {
77
+ if (currentModal.current !== modal) return;
78
+ if (modal.contains(document.activeElement)) return;
79
+
80
+ const closeBtn = modal.querySelector<HTMLElement>('.cds--modal-close');
81
+ if (
82
+ closeBtn &&
83
+ window.getComputedStyle(closeBtn).visibility !== 'hidden'
84
+ ) {
85
+ closeBtn.focus();
86
+ return;
87
+ }
88
+
89
+ if (++pollAttempts < 30) {
90
+ requestAnimationFrame(pollAndFocus);
91
+ }
92
+ };
93
+ requestAnimationFrame(pollAndFocus);
94
+
95
+ keydownHandler.current = (event: KeyboardEvent) => {
96
+ if (event.key !== 'Tab') return;
97
+
98
+ const focusable = getFocusableElements(modal);
99
+ if (focusable.length === 0) {
100
+ event.preventDefault();
101
+ return;
102
+ }
103
+
104
+ if (focusable.length === 1) {
105
+ event.preventDefault();
106
+ if (focusable[0] !== document.activeElement) {
107
+ focusable[0].focus();
108
+ }
109
+ return;
110
+ }
111
+
112
+ const firstItem = focusable[0];
113
+ const lastItem = focusable[focusable.length - 1];
114
+
115
+ if (!event.shiftKey && document.activeElement === lastItem) {
116
+ event.preventDefault();
117
+ firstItem.focus();
118
+ } else if (event.shiftKey && document.activeElement === firstItem) {
119
+ event.preventDefault();
120
+ lastItem.focus();
121
+ }
122
+ };
123
+
124
+ document.addEventListener('keydown', keydownHandler.current);
125
+ }
126
+
127
+ function handleModalClose() {
128
+ // Null out currentModal first so any pending setTimeout redirects
129
+ // (scheduled by the focusin guard) see a closed modal and bail out.
130
+ currentModal.current = null;
131
+
132
+ if (focusinHandler.current) {
133
+ document.removeEventListener('focusin', focusinHandler.current);
134
+ focusinHandler.current = null;
135
+ }
136
+
137
+ if (keydownHandler.current) {
138
+ document.removeEventListener('keydown', keydownHandler.current);
139
+ keydownHandler.current = null;
140
+ }
141
+
142
+ if (previouslyFocusedElement.current instanceof HTMLElement) {
143
+ previouslyFocusedElement.current.focus();
144
+ }
145
+ }
146
+
147
+ const observer = new MutationObserver(() => {
148
+ const visibleModal = findVisibleModal(wrapper);
149
+
150
+ if (visibleModal && !currentModal.current) {
151
+ handleModalOpen(visibleModal);
152
+ } else if (!visibleModal && currentModal.current) {
153
+ handleModalClose();
154
+ }
155
+ });
156
+
157
+ observer.observe(wrapper, {
158
+ attributes: true,
159
+ attributeFilter: ['class', 'style', 'aria-modal'],
160
+ subtree: true,
161
+ });
162
+
163
+ return () => {
164
+ observer.disconnect();
165
+ if (keydownHandler.current) {
166
+ document.removeEventListener('keydown', keydownHandler.current);
167
+ }
168
+ if (focusinHandler.current) {
169
+ document.removeEventListener('focusin', focusinHandler.current);
170
+ }
171
+ };
172
+ }, [wrapperRef]);
173
+ }
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './components/LineChart';
2
2
  export * from './components/CarbonChart';
3
+ export * from './components/ChartTable';