@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.
- package/dist/charts.js +851 -69
- package/dist/charts.js.map +1 -1
- package/dist/charts.modern.module.js +847 -72
- package/dist/charts.modern.module.js.map +1 -1
- package/dist/charts.umd.js +3088 -680
- package/dist/charts.umd.js.map +1 -1
- package/dist/components/CarbonChart/CarbonChart.d.ts +46 -0
- package/dist/components/CarbonChart/CarbonChartBar.stories.d.ts +27 -9
- 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/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 +17 -0
- package/dist/components/ChartTable/index.d.ts +14 -0
- package/dist/components/LineChart/DataTable.d.ts +1 -1
- package/dist/hooks/useCarbonModalFocusManagement.d.ts +2 -0
- package/dist/index.d.ts +1 -0
- package/package.json +5 -5
- package/src/components/CarbonChart/CarbonChart.test.js +645 -2
- package/src/components/CarbonChart/CarbonChart.tsx +950 -59
- package/src/components/CarbonChart/CarbonChartArea.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartAreaStacked.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartBar.stories.tsx +9 -2
- package/src/components/CarbonChart/CarbonChartBarFloating.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartBarGrouped.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartBarStacked.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartBoxplot.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartBubble.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartBullet.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartCombo.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartDonut.stories.tsx +3 -1
- package/src/components/CarbonChart/CarbonChartGauge.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartHistogram.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartLine.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartLollipop.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartMeter.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartPie.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartRadar.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartScatter.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartSparkline.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartStep.stories.tsx +1 -1
- package/src/components/ChartTable/ChartDataTable.tsx +72 -0
- package/src/components/ChartTable/ChartFullscreenButton.tsx +59 -0
- package/src/components/ChartTable/ChartMoreOptionsButton.tsx +48 -0
- package/src/components/ChartTable/ChartTable.stories.tsx +152 -0
- package/src/components/ChartTable/ChartTable.test.js +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 +59 -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/hooks/useCarbonModalFocusManagement.ts +173 -0
- 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={
|
|
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