@react-magma/charts 14.0.0-rc.3 → 14.0.0-rc.5

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 (33) hide show
  1. package/dist/charts.js +574 -7
  2. package/dist/charts.js.map +1 -1
  3. package/dist/charts.modern.module.js +569 -9
  4. package/dist/charts.modern.module.js.map +1 -1
  5. package/dist/charts.umd.js +6711 -366
  6. package/dist/charts.umd.js.map +1 -1
  7. package/dist/components/CarbonChart/CarbonChart.d.ts +41 -0
  8. package/dist/components/ChartTable/ChartDataTable.d.ts +19 -0
  9. package/dist/components/ChartTable/ChartFullscreenButton.d.ts +26 -0
  10. package/dist/components/ChartTable/ChartMoreOptionsButton.d.ts +18 -0
  11. package/dist/components/ChartTable/ChartTable.test.d.ts +1 -0
  12. package/dist/components/ChartTable/ChartTableButton.d.ts +24 -0
  13. package/dist/components/ChartTable/ChartTableModal.d.ts +44 -0
  14. package/dist/components/ChartTable/ChartToolbar.d.ts +19 -0
  15. package/dist/components/ChartTable/chartToolbarI18n.d.ts +16 -0
  16. package/dist/components/ChartTable/index.d.ts +14 -0
  17. package/dist/hooks/useCarbonModalFocusManagement.d.ts +2 -0
  18. package/dist/index.d.ts +1 -0
  19. package/package.json +11 -6
  20. package/src/components/CarbonChart/CarbonChart.test.js +318 -2
  21. package/src/components/CarbonChart/CarbonChart.tsx +640 -15
  22. package/src/components/ChartTable/ChartDataTable.tsx +75 -0
  23. package/src/components/ChartTable/ChartFullscreenButton.tsx +59 -0
  24. package/src/components/ChartTable/ChartMoreOptionsButton.tsx +48 -0
  25. package/src/components/ChartTable/ChartTable.stories.tsx +152 -0
  26. package/src/components/ChartTable/ChartTable.test.tsx +452 -0
  27. package/src/components/ChartTable/ChartTableButton.tsx +56 -0
  28. package/src/components/ChartTable/ChartTableModal.tsx +135 -0
  29. package/src/components/ChartTable/ChartToolbar.tsx +51 -0
  30. package/src/components/ChartTable/chartToolbarI18n.ts +55 -0
  31. package/src/components/ChartTable/index.ts +23 -0
  32. package/src/hooks/useCarbonModalFocusManagement.ts +173 -0
  33. package/src/index.ts +1 -0
@@ -0,0 +1,55 @@
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
+ makeFullScreen: string;
12
+ moreOptionsAriaLabel: string;
13
+ showAsTableTooltip: string;
14
+ tabularRepresentationLabel: string;
15
+ }
16
+
17
+ const defaults: ChartToolbarI18n = {
18
+ defaultTitle: 'Chart',
19
+ downloadAsCsv: 'Download as CSV',
20
+ downloadAsJpg: 'Download as JPG',
21
+ downloadAsPng: 'Download as PNG',
22
+ exitFullScreen: 'Exit full screen',
23
+ makeFullScreen: 'Make full screen',
24
+ moreOptionsAriaLabel: 'More options',
25
+ showAsTableTooltip: 'Show as table',
26
+ tabularRepresentationLabel: 'Tabular representation',
27
+ };
28
+
29
+ /**
30
+ * Reads chart toolbar i18n strings from the I18nContext if available,
31
+ * falling back to English defaults.
32
+ */
33
+
34
+ export function useChartToolbarI18n(): ChartToolbarI18n {
35
+ const i18n = React.useContext(I18nContext);
36
+ const toolbar = (i18n.charts as { toolbar?: Partial<ChartToolbarI18n> })
37
+ ?.toolbar;
38
+
39
+ if (!toolbar) return defaults;
40
+
41
+ return {
42
+ defaultTitle: toolbar.defaultTitle ?? defaults.defaultTitle,
43
+ downloadAsCsv: toolbar.downloadAsCsv ?? defaults.downloadAsCsv,
44
+ downloadAsJpg: toolbar.downloadAsJpg ?? defaults.downloadAsJpg,
45
+ downloadAsPng: toolbar.downloadAsPng ?? defaults.downloadAsPng,
46
+ exitFullScreen: toolbar.exitFullScreen ?? defaults.exitFullScreen,
47
+ makeFullScreen: toolbar.makeFullScreen ?? defaults.makeFullScreen,
48
+ moreOptionsAriaLabel:
49
+ toolbar.moreOptionsAriaLabel ?? defaults.moreOptionsAriaLabel,
50
+ showAsTableTooltip:
51
+ toolbar.showAsTableTooltip ?? defaults.showAsTableTooltip,
52
+ tabularRepresentationLabel:
53
+ toolbar.tabularRepresentationLabel ?? defaults.tabularRepresentationLabel,
54
+ };
55
+ }
@@ -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';
@@ -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 +1,2 @@
1
1
  export * from './components/CarbonChart';
2
+ export * from './components/ChartTable';