Skip to content

Commit f40fee5

Browse files
author
Becca Bailey
authored
Merge pull request #2316 from FormidableLabs/feature/victory-bar-beta
Add Victory Bar v37
2 parents 5d692c0 + 8ac4793 commit f40fee5

21 files changed

+657
-57
lines changed

.github/workflows/chromatic.yml

+1-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ on:
77
branches:
88
- main
99
paths:
10-
- 'stories/**'
11-
- 'packages/**'
10+
- "stories/**"
1211

1312
jobs:
1413
chromatic:

.storybook/main.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ module.exports = {
1515
},
1616
},
1717
],
18-
stories: ["../stories/*.stories.(js|jsx|ts|tsx)"],
18+
stories: ["../**/*.stories.(js|jsx|ts|tsx)"],
1919
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/* eslint-disable react/no-multi-comp */
2+
import * as React from "react";
3+
import VictoryBar from "./victory-bar";
4+
import VictoryChart from "victory-chart/lib/v37/victory-chart";
5+
6+
export default {
7+
title: "v37/VictoryBar",
8+
component: VictoryBar,
9+
};
10+
11+
export const Demo = (props) => {
12+
return <VictoryBar {...props} />;
13+
};
14+
15+
Demo.args = {
16+
barRatio: 0.5,
17+
};
18+
19+
export const WithChart = (props) => {
20+
return (
21+
<VictoryChart {...props}>
22+
<VictoryBar />
23+
</VictoryChart>
24+
);
25+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
import * as React from "react";
2+
import { render, fireEvent, screen } from "@testing-library/react";
3+
import { range } from "lodash";
4+
import { VictoryChart } from "victory-chart";
5+
import { Bar, VictoryBar } from "victory-bar";
6+
import { isBar, getBarHeight } from "../../../../test/helpers";
7+
8+
describe("components/victory-bar", () => {
9+
describe("default component rendering", () => {
10+
it("attaches safe user props to the container component", () => {
11+
render(
12+
<VictoryBar
13+
data-testid="victory-bar"
14+
aria-label="Chart"
15+
unsafe-prop="test"
16+
/>,
17+
);
18+
19+
const container = screen.getByTestId("victory-bar");
20+
expect(screen.getByLabelText("Chart")).toBeDefined();
21+
expect(container).not.toHaveAttribute("unsafe-prop");
22+
expect(container.tagName).toEqual("svg");
23+
});
24+
25+
it("attaches safe user props to the group component if the component is rendered inside a VictoryChart", () => {
26+
render(
27+
<VictoryBar
28+
data-testid="victory-bar"
29+
aria-label="Chart"
30+
unsafe-prop="test"
31+
/>,
32+
{ wrapper: VictoryChart },
33+
);
34+
35+
const container = screen.getByTestId("victory-bar");
36+
expect(screen.getByLabelText("Chart")).toBeDefined();
37+
expect(container).not.toHaveAttribute("unsafe-prop");
38+
expect(container.tagName).toEqual("g");
39+
});
40+
41+
it("renders an svg with the correct width and height", () => {
42+
const { container } = render(<VictoryBar />);
43+
const svg = container.querySelector("svg");
44+
expect(svg!.getAttribute("style")).toContain("width: 100%; height: 100%");
45+
});
46+
47+
it("renders an svg with the correct viewBox", () => {
48+
const { container } = render(<VictoryBar />);
49+
const svg = container.querySelector("svg");
50+
const viewBoxValue = `0 0 ${450} ${300}`;
51+
expect(svg!.getAttribute("viewBox")).toEqual(viewBoxValue);
52+
});
53+
54+
it("renders 4 bars", () => {
55+
const { container } = render(<VictoryBar />);
56+
const bars = container.querySelectorAll("path");
57+
expect(bars.length).toEqual(4);
58+
});
59+
60+
it("renders each bar as a rectangle", () => {
61+
const { container } = render(<VictoryBar />);
62+
const barCommandStrings = Array.from(
63+
container.querySelectorAll("path"),
64+
).map((bar) => bar.getAttribute("d"));
65+
barCommandStrings.forEach((commandString) => {
66+
expect(isBar(commandString)).toBeTruthy();
67+
});
68+
});
69+
});
70+
71+
describe("rendering data", () => {
72+
it("renders bars for {x, y} shaped data (default)", () => {
73+
const data = range(10).map((i) => ({ x: i, y: i }));
74+
const { container } = render(<VictoryBar data={data} />);
75+
const bars = container.querySelectorAll("path");
76+
expect(bars.length).toEqual(10);
77+
});
78+
79+
it("renders ordered bars when sortKey is passed", () => {
80+
const data = range(5)
81+
.map((i) => ({ x: i, y: i }))
82+
.reverse();
83+
const { container } = render(<VictoryBar data={data} sortKey="x" />);
84+
const barHeight = Array.from(container.querySelectorAll("path")).map(
85+
(bar) => {
86+
const commandString = bar.getAttribute("d");
87+
return getBarHeight(commandString);
88+
},
89+
);
90+
91+
const ascendingBars = [...barHeight].sort((a, b) => a - b);
92+
93+
expect(barHeight).toEqual(ascendingBars);
94+
});
95+
96+
it("renders reverse ordered bars when sortOrder is descending", () => {
97+
const data = range(5)
98+
.map((i) => ({ x: i, y: i }))
99+
.reverse();
100+
const { container } = render(
101+
<VictoryBar data={data} sortKey="x" sortOrder="descending" />,
102+
);
103+
const barHeight = Array.from(container.querySelectorAll("path")).map(
104+
(bar) => {
105+
const commandString = bar.getAttribute("d");
106+
return getBarHeight(commandString);
107+
},
108+
);
109+
const descendingBars = [...barHeight].sort((a, b) => b - a);
110+
111+
expect(barHeight).toEqual(descendingBars);
112+
});
113+
114+
it("renders bars for array-shaped data", () => {
115+
const data = range(20).map((i) => [i, i]);
116+
const { container } = render(<VictoryBar data={data} x={0} y={1} />);
117+
const bars = container.querySelectorAll("path");
118+
expect(bars).toHaveLength(20);
119+
});
120+
121+
it("renders bars for deeply-nested data", () => {
122+
const data = range(8).map((i) => ({ a: { b: [{ x: i, y: i }] } }));
123+
const { container } = render(
124+
<VictoryBar data={data} x="a.b[0].x" y="a.b[0].y" />,
125+
);
126+
const bars = container.querySelectorAll("path");
127+
expect(bars).toHaveLength(8);
128+
});
129+
130+
it("renders bars values with null accessor", () => {
131+
const data = range(8);
132+
const { container } = render(
133+
// @ts-expect-error null is not assignable to DataGetterType
134+
<VictoryBar data={data} x={null} y={null} />,
135+
);
136+
const bars = container.querySelectorAll("path");
137+
expect(bars).toHaveLength(8);
138+
});
139+
140+
it("renders bars with appropriate relative heights", () => {
141+
const { container } = render(
142+
<VictoryBar
143+
data={[
144+
{ x: 1, y: 1 },
145+
{ x: 2, y: 2 },
146+
{ x: 3, y: 3 },
147+
]}
148+
/>,
149+
);
150+
const bars = Array.from(container.querySelectorAll("path"));
151+
const heights = bars.map((bar) => {
152+
const commandString = bar.getAttribute("d");
153+
return getBarHeight(commandString);
154+
});
155+
156+
expect(Math.trunc(heights[1] / 2)).toEqual(Math.trunc(heights[0]));
157+
expect(((heights[2] / 3) * 2).toFixed(3)).toEqual(heights[1].toFixed(3));
158+
});
159+
160+
it("does not render data with null x or y values", () => {
161+
const data = [
162+
{ x: 1, y: 2 },
163+
{ x: null, y: 4 },
164+
{ x: 5, y: null },
165+
];
166+
const { container } = render(<VictoryBar data={data} />);
167+
expect(container.querySelectorAll("path")).toHaveLength(1);
168+
});
169+
});
170+
171+
describe.skip("event handling", () => {
172+
const clickHandler = jest.fn();
173+
174+
beforeEach(() => {
175+
clickHandler.mockReset();
176+
});
177+
178+
it("attaches an event to the parent svg", () => {
179+
const { container } = render(
180+
<VictoryBar
181+
events={[
182+
{
183+
target: "parent",
184+
eventHandlers: { onClick: clickHandler },
185+
},
186+
]}
187+
/>,
188+
);
189+
const bar = container.querySelector("path");
190+
fireEvent.click(bar!);
191+
192+
expect(clickHandler).toHaveBeenCalled();
193+
});
194+
195+
it("attaches an event to data", () => {
196+
const data = [
197+
{ x: 0, y: 0, label: "0" },
198+
{ x: 1, y: 1, label: "1" },
199+
{ x: 2, y: 2, label: "2" },
200+
];
201+
const { container } = render(
202+
<VictoryBar
203+
data={data}
204+
events={[
205+
{
206+
target: "data",
207+
eventHandlers: { onClick: clickHandler },
208+
},
209+
]}
210+
/>,
211+
);
212+
const bars = container.querySelectorAll("path");
213+
bars.forEach((bar, index) => {
214+
clickHandler.mockReset();
215+
fireEvent.click(bar);
216+
expect(clickHandler).toHaveBeenCalled();
217+
218+
const barContext = clickHandler.mock.calls[0][1];
219+
expect(barContext.datum.x).toEqual(data[index].x);
220+
});
221+
});
222+
223+
it("attaches an event to a label", () => {
224+
const data = [
225+
{ x: 0, y: 0, label: "label 0" },
226+
{ x: 1, y: 1, label: "label 1" },
227+
{ x: 2, y: 2, label: "label 2" },
228+
];
229+
230+
render(
231+
<VictoryBar
232+
data={data}
233+
labels={({ datum }) => datum.label}
234+
events={[
235+
{
236+
target: "labels",
237+
eventHandlers: { onClick: clickHandler },
238+
},
239+
]}
240+
/>,
241+
);
242+
const label = screen.getByText("label 1");
243+
244+
fireEvent.click(label);
245+
246+
expect(clickHandler).toHaveBeenCalled();
247+
});
248+
});
249+
250+
describe("accessibility", () => {
251+
it("adds an aria role to each bar in the series", () => {
252+
const data = range(10).map((y, x) => ({ x, y }));
253+
render(<VictoryBar data={data} />);
254+
const presentationElements = screen.getAllByRole("presentation");
255+
expect(presentationElements).toHaveLength(11); // bars plus container
256+
});
257+
258+
it("applies aria-label and tabIndex to the Bar primitive", () => {
259+
const data = range(5, 11).map((y, x) => ({ y, x }));
260+
const { container } = render(
261+
<VictoryBar
262+
data={data}
263+
dataComponent={
264+
<Bar
265+
ariaLabel={({ datum }) => `x: ${datum.x}`}
266+
tabIndex={({ index }) => (index as number) + 1}
267+
/>
268+
}
269+
/>,
270+
);
271+
272+
container.querySelectorAll("path").forEach((bar, index) => {
273+
expect(parseInt(bar.getAttribute("tabindex")!)).toEqual(index + 1);
274+
expect(bar.getAttribute("aria-label")).toEqual(`x: ${data[index].x}`);
275+
});
276+
});
277+
});
278+
});

0 commit comments

Comments
 (0)