Skip to content

Commit da339ef

Browse files
committed
chore(exploration): init commit
1 parent 012fc44 commit da339ef

File tree

5 files changed

+323
-1
lines changed

5 files changed

+323
-1
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { Box } from "@twilio-paste/box";
2+
import Highcharts, { Chart, Options } from "highcharts";
3+
import HighchartsReact from "highcharts-react-official";
4+
import HighchartsAccessibilityModule from "highcharts/modules/accessibility";
5+
import React, { FC, memo, useEffect, useLayoutEffect, useRef, useState } from "react";
6+
import { applyPasteHighchartsModules } from "../applyPasteHighchartsModules";
7+
import { usePasteHighchartsTheme } from "../usePasteHighchartsTheme";
8+
import { useChartContext } from "./ChartContext";
9+
10+
export const TextBox = () => {
11+
return <Box backgroundColor="colorBackgroundAvailable">Text</Box>;
12+
};
13+
14+
const useTooltipTracking = (options: Highcharts.Options): Highcharts.Options => {
15+
const context = useChartContext();
16+
17+
const series = options.series?.map((s) => {
18+
return {
19+
...s,
20+
point: {
21+
...s.point,
22+
events: {
23+
mouseOver: function (this) {
24+
context.setActivePoint(this);
25+
},
26+
mouseOut: function (this) {
27+
// context.setActivePoint(null);
28+
},
29+
click: function (this) {
30+
const actions = document.getElementById("tooltip-actions");
31+
if (actions) {
32+
context.chart?.tooltip.hide();
33+
this.select(false,true);
34+
this.graphic.element.blur()
35+
actions.focus();
36+
}
37+
},
38+
},
39+
},
40+
};
41+
});
42+
43+
return {
44+
...options,
45+
series,
46+
};
47+
};
48+
49+
const Chart: FC<{
50+
options: Options;
51+
}> = ({ options }) => {
52+
applyPasteHighchartsModules(Highcharts, HighchartsAccessibilityModule);
53+
54+
const [chartOptions, setChartOptions] = useState<Options>(usePasteHighchartsTheme(useTooltipTracking(options)));
55+
const chartContext = useChartContext();
56+
const chartRef = useRef(null);
57+
58+
useLayoutEffect(() => {
59+
setChartOptions(options);
60+
}, [options]);
61+
62+
// if setting the chart too early before data loaded, events throughout charting run into issues
63+
const waitForSeriesToBeLoaded = (chart: Highcharts.Chart) => {
64+
if (chart.series?.length === 0) {
65+
setTimeout(() => waitForSeriesToBeLoaded(chart), 100);
66+
} else {
67+
chartContext.setChart(chart);
68+
}
69+
};
70+
71+
useEffect(() => {
72+
if (chartRef.current) {
73+
// @ts-ignore
74+
waitForSeriesToBeLoaded(chartRef.current.chart, chartContext.setChart);
75+
}
76+
}, [chartRef.current]);
77+
78+
const callback = (chart: Chart) => {
79+
// This works with loaded series
80+
console.log("chart callback", chart?.series?.length);
81+
};
82+
83+
return (
84+
<Box width="100%" id="example-chart-container" padding="space100" data-testid="highcharts-container">
85+
<HighchartsReact
86+
highcharts={Highcharts}
87+
options={chartOptions}
88+
containerProps={{ style: { width: "100%", overflow: "visible" } }}
89+
ref={chartRef}
90+
constructorType={chartOptions.chart?.map ? "mapChart" : undefined}
91+
updateArgs={[true, true, false]}
92+
callback={callback}
93+
/>
94+
</Box>
95+
);
96+
};
97+
98+
export const BaseChart = memo(Chart);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Chart, OptionsStackingValue, Point } from "highcharts";
2+
import React, { useEffect, useState } from "react";
3+
4+
export interface ChartContextProps {
5+
chartId?: string;
6+
legendTextFormatter?: (name: string) => string;
7+
disableLegends?: boolean;
8+
isLight?: boolean;
9+
hideLegends?: boolean;
10+
columnStackingType?: OptionsStackingValue;
11+
children?: React.ReactNode;
12+
}
13+
14+
interface ChartProviderProps extends ChartContextProps {
15+
chart?: Chart;
16+
activePoint: Point | null;
17+
setActivePoint(p: Point | null): void;
18+
setChart(chart: Chart): void;
19+
}
20+
21+
export const ChartContext = React.createContext<ChartProviderProps>({
22+
activePoint: null,
23+
setActivePoint: () => {},
24+
setChart: () => {},
25+
});
26+
27+
export const useChartContext = () => React.useContext(ChartContext);
28+
29+
export const ChartContextProvider: React.FC<ChartContextProps> = ({
30+
children,
31+
chartId,
32+
legendTextFormatter,
33+
disableLegends = false,
34+
hideLegends = false,
35+
}) => {
36+
const [activePoint, setActivePoint] = useState<Point | null>(null);
37+
const [chart, setChart] = useState<Chart>();
38+
39+
useEffect(() => {
40+
}, [activePoint]);
41+
42+
return (
43+
<ChartContext.Provider
44+
value={{
45+
chart,
46+
setChart,
47+
activePoint,
48+
setActivePoint,
49+
chartId,
50+
legendTextFormatter,
51+
disableLegends,
52+
hideLegends,
53+
}}
54+
>
55+
{children}
56+
</ChartContext.Provider>
57+
);
58+
};

packages/paste-libraries/data-visualization/src/usePasteHighchartsTheme.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ export const usePasteHighchartsTheme = (options: Highcharts.Options): Highcharts
4242
colors,
4343
chart: {
4444
backgroundColor: context.backgroundColors.colorBackgroundBody,
45+
style: {
46+
fontFamily: context.fonts.fontFamilyText,
47+
},
4548
},
4649
title: {
4750
style: {
@@ -105,7 +108,12 @@ export const usePasteHighchartsTheme = (options: Highcharts.Options): Highcharts
105108
},
106109
},
107110
tooltip: {
108-
backgroundColor: context.backgroundColors.colorBackground,
111+
112+
backgroundColor: context.backgroundColors.colorBackgroundBody,
113+
borderColor: context.borderColors.colorBorderWeaker,
114+
borderWidth: context.borderWidths.borderWidth10,
115+
borderRadius: context.radii.borderRadius30.replace("px", ""),
116+
padding: 12,
109117
style: {
110118
color: context.textColors.colorText,
111119
},
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { Meta, StoryFn } from "@storybook/react";
2+
import { Button } from "@twilio-paste/button";
3+
import { Heading } from "@twilio-paste/heading";
4+
import { ListItem, UnorderedList } from "@twilio-paste/list";
5+
import { Paragraph } from "@twilio-paste/paragraph";
6+
import { Popover, PopoverArrow, PopoverDisclosure, usePopoverState } from "@twilio-paste/paste-reakit-fork";
7+
import { Stack } from "@twilio-paste/stack";
8+
import { Tooltip } from "@twilio-paste/tooltip";
9+
import Highcharts, { SeriesOptionsType } from "highcharts";
10+
import HighchartsReact from "highcharts-react-official";
11+
import HighchartsAccessibilityModule from "highcharts/modules/accessibility";
12+
import React, { useEffect } from "react";
13+
import { applyPasteHighchartsModules, usePasteHighchartsTheme } from "../src";
14+
import { BaseChart } from "../src/exploration/BaseChart";
15+
import { ChartContextProvider, useChartContext } from "../src/exploration/ChartContext";
16+
import { lineChartOptions } from "./options/lineChartOptions";
17+
import { createPortal } from "react-dom";
18+
// eslint-disable-next-line import/no-default-export
19+
export default {
20+
title: "Libraries/data-visualization/exploration",
21+
parameters: {
22+
chromatic: { disableSnapshot: true },
23+
a11y: {
24+
// no need to a11y check composition of a11y checked components
25+
disable: true,
26+
},
27+
},
28+
} as Meta;
29+
30+
export const Notes: StoryFn = () => {
31+
const popover = usePopoverState({ placement: "right-start" });
32+
return (
33+
<Stack orientation="vertical" spacing="space100">
34+
<Heading as="h1" variant="heading10">
35+
Highcharts Accessibility Exploration
36+
</Heading>
37+
<Paragraph>
38+
The purpose of this story is to explore the accessibility features of Highcharts and determine how we can apply
39+
paste components while still mainitaining accessible features in Hightcharts.
40+
</Paragraph>
41+
<Heading as="h2" variant="heading20">
42+
Notes for Highcharts a11y:
43+
</Heading>
44+
<UnorderedList>
45+
<ListItem>When switching series, reads new series</ListItem>
46+
<ListItem>
47+
When switching points, reads point index and x axis then value. 2 (point 2) x 3 (x axis = 3) 8105 (y value)
48+
Installation [series name]
49+
</ListItem>
50+
<ListItem>Switching from series the legend items announce they are used to toggle series</ListItem>
51+
<ListItem>When first focusing on the chart it will read the title specified in options</ListItem>
52+
<ListItem>
53+
When first focusing on the chart it will focus on series or legend items. By navigating up the node it can
54+
read the meta about that chart including description in the options provided
55+
</ListItem>
56+
</UnorderedList>
57+
58+
<Heading as="h2" variant="heading20">
59+
Important findings:
60+
</Heading>
61+
<UnorderedList>
62+
<ListItem>The tooltip is not read. Instead the point information is announced</ListItem>
63+
<ListItem>The tooltips are not focusable </ListItem>
64+
</UnorderedList>
65+
66+
<>
67+
<PopoverDisclosure {...popover}>Open Popover</PopoverDisclosure>
68+
<Popover {...popover} aria-label="Welcome">
69+
<PopoverArrow {...popover} />
70+
Welcome to Reakit!
71+
</Popover>
72+
</>
73+
</Stack>
74+
);
75+
};
76+
77+
const AdditionalTooltipComp = () => {
78+
const { activePoint, chart } = useChartContext();
79+
80+
useEffect(() => {
81+
}, [activePoint]);
82+
83+
if (!activePoint || !chart) return null;
84+
85+
const { x, y } = activePoint.graphic.element.getBoundingClientRect();
86+
87+
return createPortal(
88+
<div style={{ width: 200, height: 200, backgroundColor: "red", position: "absolute", left: x, top: y, zIndex: 10 }}>
89+
<strong>In Portal</strong>
90+
<br />
91+
Text to display
92+
<div id="tooltip-actions" tabIndex={-1}
93+
onBlur={(e) => {
94+
if(!e.currentTarget.contains(e.relatedTarget)){
95+
e.preventDefault();
96+
e.stopPropagation();
97+
activePoint.graphic.element.focus();
98+
}
99+
}}
100+
101+
>
102+
<button
103+
tabIndex={0}
104+
onClick={(e) => {
105+
e.stopPropagation();
106+
alert(activePoint.series.name + "clicked");
107+
}}
108+
onBlur={(e) => {
109+
}}
110+
>
111+
View More Details
112+
</button>
113+
<button
114+
tabIndex={0}
115+
onClick={(e) => {
116+
e.stopPropagation();
117+
alert(activePoint.series.name + "clicked");
118+
}}
119+
onBlur={(e) => {
120+
}}
121+
>
122+
View More Details
123+
</button>
124+
</div>
125+
</div>,
126+
document.getElementById("example-chart-container")
127+
);
128+
};
129+
130+
export const TooltipScreenReaderFuncitonality: StoryFn = () => {
131+
return (
132+
<Stack orientation="vertical" spacing="space100">
133+
<ChartContextProvider chartId="test">
134+
<BaseChart options={lineChartOptions} key="chart1" />
135+
<AdditionalTooltipComp />
136+
</ChartContextProvider>
137+
</Stack>
138+
);
139+
};

packages/paste-libraries/data-visualization/stories/options/lineChartOptions.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,35 @@
11
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
22
// @ts-nocheck copied from official highcharts docs
33
import type Highcharts from "highcharts";
4+
import React from "react";
45

56
export const lineChartOptions: Highcharts.Options = {
67
accessibility: {
78
description: "This chart shows the solar employment growth by sector from 2010 to 2016.",
9+
point: {
10+
descriptionFormatter: function (point) {
11+
return `${point.index + 1}. ${point.x}, ${point.y} ${point.series.name}. ${JSON.stringify(
12+
point.series.markerAttribs(point),
13+
)}`;
14+
},
15+
16+
},
817
},
18+
919
title: {
1020
text: "Solar Employment Growth by Sector, 2010-2016",
21+
style: {
22+
display: "none",
23+
},
1124
},
1225
subtitle: {
1326
text: "Source: thesolarfoundation.com",
27+
style: {
28+
display: "none",
29+
},
30+
},
31+
tooltip: {
32+
shared: true,
1433
},
1534
series: [
1635
{

0 commit comments

Comments
 (0)