@react-magma/charts 15.0.0-rc.1 → 15.0.0-rc.2
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 +62 -6
- package/dist/charts.js.map +1 -1
- package/dist/charts.modern.module.js +62 -6
- package/dist/charts.modern.module.js.map +1 -1
- package/dist/charts.umd.js +64 -8
- package/dist/charts.umd.js.map +1 -1
- package/dist/components/ChartTable/chartToolbarI18n.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/CarbonChart/CarbonChart.test.js +134 -1
- package/src/components/CarbonChart/CarbonChart.tsx +87 -0
- package/src/components/ChartTable/chartToolbarI18n.ts +4 -0
package/package.json
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
3
|
import { act, render, screen, fireEvent } from '@testing-library/react';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ThemeContext,
|
|
6
|
+
magma,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
I18nContext,
|
|
9
|
+
defaultI18n,
|
|
10
|
+
} from 'react-magma-dom';
|
|
5
11
|
|
|
6
12
|
import { CarbonChart, CarbonChartType } from '.';
|
|
7
13
|
|
|
@@ -895,4 +901,131 @@ describe('CarbonChart', () => {
|
|
|
895
901
|
).toBeInTheDocument();
|
|
896
902
|
});
|
|
897
903
|
});
|
|
904
|
+
|
|
905
|
+
describe('Legend accessibility wrap', () => {
|
|
906
|
+
const testId = 'legend-a11y';
|
|
907
|
+
|
|
908
|
+
it('wraps the Carbon legend in a fieldset with a legend caption derived from the chart title', () => {
|
|
909
|
+
const { getByTestId } = render(
|
|
910
|
+
<CarbonChart
|
|
911
|
+
testId={testId}
|
|
912
|
+
dataSet={dataSet}
|
|
913
|
+
options={chartOptions}
|
|
914
|
+
type={CarbonChartType.bar}
|
|
915
|
+
/>
|
|
916
|
+
);
|
|
917
|
+
const wrapper = getByTestId(testId);
|
|
918
|
+
|
|
919
|
+
const fieldset = wrapper.querySelector('.cds--cc--legend-fieldset');
|
|
920
|
+
expect(fieldset).not.toBeNull();
|
|
921
|
+
expect(fieldset.tagName).toBe('FIELDSET');
|
|
922
|
+
expect(fieldset.querySelector('.cds--cc--legend')).not.toBeNull();
|
|
923
|
+
|
|
924
|
+
const caption = fieldset.querySelector('legend');
|
|
925
|
+
expect(caption).not.toBeNull();
|
|
926
|
+
expect(caption.textContent).toBe(
|
|
927
|
+
`${chartOptions.title}. Checking these checkboxes will update the chart.`
|
|
928
|
+
);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
it('uses ariaLabel for the legend caption when provided', () => {
|
|
932
|
+
const ariaLabel = 'Quarterly sales chart';
|
|
933
|
+
const { getByTestId } = render(
|
|
934
|
+
<CarbonChart
|
|
935
|
+
testId={testId}
|
|
936
|
+
ariaLabel={ariaLabel}
|
|
937
|
+
dataSet={dataSet}
|
|
938
|
+
options={chartOptions}
|
|
939
|
+
type={CarbonChartType.bar}
|
|
940
|
+
/>
|
|
941
|
+
);
|
|
942
|
+
const wrapper = getByTestId(testId);
|
|
943
|
+
|
|
944
|
+
const caption = wrapper.querySelector(
|
|
945
|
+
'.cds--cc--legend-fieldset > legend'
|
|
946
|
+
);
|
|
947
|
+
expect(caption).not.toBeNull();
|
|
948
|
+
expect(caption.textContent).toBe(
|
|
949
|
+
`${ariaLabel}. Checking these checkboxes will update the chart.`
|
|
950
|
+
);
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
it('does not duplicate the fieldset when the mutation observer fires again with no DOM changes', () => {
|
|
954
|
+
const { getByTestId } = render(
|
|
955
|
+
<CarbonChart
|
|
956
|
+
testId={testId}
|
|
957
|
+
dataSet={dataSet}
|
|
958
|
+
options={chartOptions}
|
|
959
|
+
type={CarbonChartType.bar}
|
|
960
|
+
/>
|
|
961
|
+
);
|
|
962
|
+
const wrapper = getByTestId(testId);
|
|
963
|
+
|
|
964
|
+
const before = wrapper.querySelectorAll('.cds--cc--legend-fieldset');
|
|
965
|
+
expect(before).toHaveLength(1);
|
|
966
|
+
|
|
967
|
+
act(() => {
|
|
968
|
+
mutationObserverCallback?.();
|
|
969
|
+
mutationObserverCallback?.();
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
const after = wrapper.querySelectorAll('.cds--cc--legend-fieldset');
|
|
973
|
+
expect(after).toHaveLength(1);
|
|
974
|
+
expect(after[0]).toBe(before[0]);
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
it('replaces Carbon\'s generic "Data groups" aria-label with semantic list roles on the legend and its items', () => {
|
|
978
|
+
const { getByTestId } = render(
|
|
979
|
+
<CarbonChart
|
|
980
|
+
testId={testId}
|
|
981
|
+
dataSet={dataSet}
|
|
982
|
+
options={chartOptions}
|
|
983
|
+
type={CarbonChartType.bar}
|
|
984
|
+
/>
|
|
985
|
+
);
|
|
986
|
+
const wrapper = getByTestId(testId);
|
|
987
|
+
|
|
988
|
+
const legend = wrapper.querySelector('.cds--cc--legend');
|
|
989
|
+
expect(legend).not.toBeNull();
|
|
990
|
+
expect(legend.getAttribute('aria-label')).toBeNull();
|
|
991
|
+
expect(legend.getAttribute('role')).toBe('list');
|
|
992
|
+
|
|
993
|
+
const items = legend.querySelectorAll('.legend-item');
|
|
994
|
+
expect(items.length).toBeGreaterThan(0);
|
|
995
|
+
items.forEach(item => {
|
|
996
|
+
expect(item.getAttribute('role')).toBe('listitem');
|
|
997
|
+
});
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
it('uses translated legend instructions from I18nContext', () => {
|
|
1001
|
+
const translatedInstructions =
|
|
1002
|
+
'Marquer ces cases mettra à jour le graphique.';
|
|
1003
|
+
const i18nValue = {
|
|
1004
|
+
...defaultI18n,
|
|
1005
|
+
charts: {
|
|
1006
|
+
...defaultI18n.charts,
|
|
1007
|
+
toolbar: { legendInstructions: translatedInstructions },
|
|
1008
|
+
},
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
const { getByTestId } = render(
|
|
1012
|
+
<I18nContext.Provider value={i18nValue}>
|
|
1013
|
+
<CarbonChart
|
|
1014
|
+
testId={testId}
|
|
1015
|
+
dataSet={dataSet}
|
|
1016
|
+
options={chartOptions}
|
|
1017
|
+
type={CarbonChartType.bar}
|
|
1018
|
+
/>
|
|
1019
|
+
</I18nContext.Provider>
|
|
1020
|
+
);
|
|
1021
|
+
const wrapper = getByTestId(testId);
|
|
1022
|
+
|
|
1023
|
+
const caption = wrapper.querySelector(
|
|
1024
|
+
'.cds--cc--legend-fieldset > legend'
|
|
1025
|
+
);
|
|
1026
|
+
expect(caption.textContent).toBe(
|
|
1027
|
+
`${chartOptions.title}. ${translatedInstructions}`
|
|
1028
|
+
);
|
|
1029
|
+
});
|
|
1030
|
+
});
|
|
898
1031
|
});
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
} from 'react-magma-icons';
|
|
39
39
|
|
|
40
40
|
import { useCarbonModalFocusManagement } from '../../hooks/useCarbonModalFocusManagement';
|
|
41
|
+
// @ts-ignore
|
|
41
42
|
import './carbon-charts.css';
|
|
42
43
|
import {
|
|
43
44
|
ChartFullscreenButton,
|
|
@@ -161,6 +162,23 @@ const CarbonChartWrapper = styled.div<{
|
|
|
161
162
|
groupsLength: number;
|
|
162
163
|
theme: ThemeInterface;
|
|
163
164
|
}>`
|
|
165
|
+
.cds--cc--legend-fieldset {
|
|
166
|
+
border: 0;
|
|
167
|
+
margin: 0;
|
|
168
|
+
min-inline-size: 0;
|
|
169
|
+
padding: 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.cds--cc--legend-fieldset > legend {
|
|
173
|
+
clip: rect(1px, 1px, 1px, 1px);
|
|
174
|
+
height: 1px;
|
|
175
|
+
overflow: hidden;
|
|
176
|
+
position: absolute;
|
|
177
|
+
top: auto;
|
|
178
|
+
white-space: nowrap;
|
|
179
|
+
width: 1px;
|
|
180
|
+
}
|
|
181
|
+
|
|
164
182
|
&:fullscreen,
|
|
165
183
|
&:-webkit-full-screen {
|
|
166
184
|
background: ${props =>
|
|
@@ -1134,6 +1152,55 @@ export const CarbonChart = React.forwardRef<HTMLDivElement, CarbonChartProps>(
|
|
|
1134
1152
|
const chartTitle: string =
|
|
1135
1153
|
(options as any).title || toolbarI18n.defaultTitle;
|
|
1136
1154
|
|
|
1155
|
+
const legendLabel = `${ariaLabel || chartTitle}. ${toolbarI18n.legendInstructions}`;
|
|
1156
|
+
|
|
1157
|
+
React.useEffect(() => {
|
|
1158
|
+
const container = internalRef.current;
|
|
1159
|
+
|
|
1160
|
+
if (!container) return;
|
|
1161
|
+
|
|
1162
|
+
const applyListSemantics = (legend: Element) => {
|
|
1163
|
+
legend.removeAttribute('aria-label');
|
|
1164
|
+
legend.setAttribute('role', 'list');
|
|
1165
|
+
legend
|
|
1166
|
+
.querySelectorAll('.legend-item')
|
|
1167
|
+
.forEach(item => item.setAttribute('role', 'listitem'));
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
const wrapLegend = () => {
|
|
1171
|
+
const legend = container.querySelector('.cds--cc--legend');
|
|
1172
|
+
|
|
1173
|
+
if (!legend) return;
|
|
1174
|
+
|
|
1175
|
+
applyListSemantics(legend);
|
|
1176
|
+
|
|
1177
|
+
const parent = legend.parentElement;
|
|
1178
|
+
|
|
1179
|
+
if (parent?.tagName === 'FIELDSET') {
|
|
1180
|
+
const existingCaption = parent.querySelector('legend');
|
|
1181
|
+
|
|
1182
|
+
if (existingCaption) existingCaption.textContent = legendLabel;
|
|
1183
|
+
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const fieldset = document.createElement('fieldset');
|
|
1188
|
+
const caption = document.createElement('legend');
|
|
1189
|
+
|
|
1190
|
+
fieldset.className = 'cds--cc--legend-fieldset';
|
|
1191
|
+
caption.textContent = legendLabel;
|
|
1192
|
+
legend.before(fieldset);
|
|
1193
|
+
fieldset.append(caption, legend);
|
|
1194
|
+
};
|
|
1195
|
+
|
|
1196
|
+
wrapLegend();
|
|
1197
|
+
const observer = new MutationObserver(wrapLegend);
|
|
1198
|
+
|
|
1199
|
+
observer.observe(container, { childList: true, subtree: true });
|
|
1200
|
+
|
|
1201
|
+
return () => observer.disconnect();
|
|
1202
|
+
}, [legendLabel]);
|
|
1203
|
+
|
|
1137
1204
|
const handleModalDownloadCsv = React.useCallback(() => {
|
|
1138
1205
|
downloadCsv(dataSet as Array<Record<string, unknown>>, chartTitle);
|
|
1139
1206
|
}, [dataSet, chartTitle]);
|
|
@@ -1203,6 +1270,26 @@ export const CarbonChart = React.forwardRef<HTMLDivElement, CarbonChartProps>(
|
|
|
1203
1270
|
|
|
1204
1271
|
const ChartType = allCharts[type] as any;
|
|
1205
1272
|
|
|
1273
|
+
React.useEffect(() => {
|
|
1274
|
+
const timer = setTimeout(() => {
|
|
1275
|
+
if (internalRef.current) {
|
|
1276
|
+
internalRef.current
|
|
1277
|
+
.querySelectorAll<SVGGElement>('g[aria-label]')
|
|
1278
|
+
.forEach(g => {
|
|
1279
|
+
const role = g.getAttribute('role');
|
|
1280
|
+
|
|
1281
|
+
if (!role) {
|
|
1282
|
+
g.setAttribute('role', 'img');
|
|
1283
|
+
} else if (role === 'graphics-object group') {
|
|
1284
|
+
g.setAttribute('role', 'graphics-object');
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
}, 0);
|
|
1289
|
+
|
|
1290
|
+
return () => clearTimeout(timer);
|
|
1291
|
+
}, [type, dataSet]);
|
|
1292
|
+
|
|
1206
1293
|
const groupsLength = Object.keys(buildColors()).length;
|
|
1207
1294
|
|
|
1208
1295
|
const showTable = chartToolbar?.showAsTable !== false;
|
|
@@ -8,6 +8,7 @@ export interface ChartToolbarI18n {
|
|
|
8
8
|
downloadAsJpg: string;
|
|
9
9
|
downloadAsPng: string;
|
|
10
10
|
exitFullScreen: string;
|
|
11
|
+
legendInstructions: string;
|
|
11
12
|
makeFullScreen: string;
|
|
12
13
|
moreOptionsAriaLabel: string;
|
|
13
14
|
showAsTableTooltip: string;
|
|
@@ -20,6 +21,7 @@ const defaults: ChartToolbarI18n = {
|
|
|
20
21
|
downloadAsJpg: 'Download as JPG',
|
|
21
22
|
downloadAsPng: 'Download as PNG',
|
|
22
23
|
exitFullScreen: 'Exit full screen',
|
|
24
|
+
legendInstructions: 'Checking these checkboxes will update the chart.',
|
|
23
25
|
makeFullScreen: 'Make full screen',
|
|
24
26
|
moreOptionsAriaLabel: 'More options',
|
|
25
27
|
showAsTableTooltip: 'Show as table',
|
|
@@ -44,6 +46,8 @@ export function useChartToolbarI18n(): ChartToolbarI18n {
|
|
|
44
46
|
downloadAsJpg: toolbar.downloadAsJpg ?? defaults.downloadAsJpg,
|
|
45
47
|
downloadAsPng: toolbar.downloadAsPng ?? defaults.downloadAsPng,
|
|
46
48
|
exitFullScreen: toolbar.exitFullScreen ?? defaults.exitFullScreen,
|
|
49
|
+
legendInstructions:
|
|
50
|
+
toolbar.legendInstructions ?? defaults.legendInstructions,
|
|
47
51
|
makeFullScreen: toolbar.makeFullScreen ?? defaults.makeFullScreen,
|
|
48
52
|
moreOptionsAriaLabel:
|
|
49
53
|
toolbar.moreOptionsAriaLabel ?? defaults.moreOptionsAriaLabel,
|