diff --git a/demo/demo-data.ts b/demo/demo-data.ts index 0ce6d4c40..ced1d633f 100644 --- a/demo/demo-data.ts +++ b/demo/demo-data.ts @@ -91,3 +91,21 @@ export const accessibilityErrorBarData = [ { x: 34, y: 65, error: 0.15 }, { x: 43, y: 50, error: 0.2 } ]; + +export const accessibilityGroupData = { + a: [ + { x: 1, y: 1 }, + { x: 2, y: 2 }, + { x: 3, y: 5 } + ], + b: [ + { x: 1, y: 2 }, + { x: 2, y: 1 }, + { x: 3, y: 7 } + ], + c: [ + { x: 1, y: 3 }, + { x: 2, y: 4 }, + { x: 3, y: 9 } + ] +}; diff --git a/demo/js/components/accessibility-demo.js b/demo/js/components/accessibility-demo.js index b18d0aedc..85023cc0a 100644 --- a/demo/js/components/accessibility-demo.js +++ b/demo/js/components/accessibility-demo.js @@ -1,6 +1,7 @@ import React from "react"; import { Curve } from "Packages/victory-line"; import { VictoryLine } from "Packages/victory-line"; +import { VictoryGroup } from "Packages/victory-group"; import { VictoryStack } from "Packages/victory-stack"; import { VictoryChart } from "Packages/victory-chart"; import { VictoryScatter } from "Packages/victory-scatter"; @@ -11,13 +12,21 @@ import { VictoryArea, Area } from "Packages/victory-area"; import { VictoryVoronoi, Voronoi } from "Packages/victory-voronoi"; import { ErrorBar, VictoryErrorBar } from "Packages/victory-errorbar"; import { Candle, VictoryCandlestick } from "Packages/victory-candlestick"; -import { LineSegment, Whisker, Border, Point, VictoryLabel } from "Packages/victory-core"; +import { + LineSegment, + Whisker, + Border, + Point, + VictoryLabel, + VictoryAccessibleGroup +} from "Packages/victory-core"; import { accessibilityBarData, accessibilityBoxData, accessibilityPieData, - accessibilityLineData, accessibilityAreaData, + accessibilityLineData, + accessibilityGroupData, accessibilityScatterData, accessibilityVoronoiData, accessibilityErrorBarData, @@ -34,14 +43,16 @@ const pageHeadingStyle = { const chartHeadingStyle = { marginBottom: "0px", - marginTop: "25px" + marginTop: "25px", + fontSize: "calc(1vw + 5px)" }; const containerStyle = { display: "flex", flexFlow: "row wrap", alignItems: "center", - justifyContent: "flex-start" + justifyContent: "flex-start", + maxWidth: "1300px" }; const chartContainerStyle = { @@ -49,7 +60,8 @@ const chartContainerStyle = { flexDirection: "column", alignItems: "center", width: "50%", - height: "50%" + height: "50%", + padding: "25px" }; export default class App extends React.Component { @@ -76,6 +88,7 @@ export default class App extends React.Component { /> + {/** BOX PLOT */}

Boxplot

@@ -293,6 +306,38 @@ export default class App extends React.Component { />
+ + {/**ACCESSIBLE GROUP */} +
+

Accessible Group

+ + + } + > + + + } + /> + + + +
); diff --git a/demo/ts/components/accessibility-demo.tsx b/demo/ts/components/accessibility-demo.tsx index 3d4af6d1d..07b0fb8f4 100644 --- a/demo/ts/components/accessibility-demo.tsx +++ b/demo/ts/components/accessibility-demo.tsx @@ -1,6 +1,7 @@ import React from "react"; import { isNumber } from "lodash"; import { Curve } from "@packages/victory-line"; +import { VictoryGroup } from "@packages/victory-group"; import { VictoryStack } from "@packages/victory-stack"; import { VictoryLine } from "@packages/victory-line"; import { VictoryBar, Bar } from "@packages/victory-bar"; @@ -12,13 +13,21 @@ import { VictoryBoxPlot } from "@packages/victory-box-plot"; import { VictoryVoronoi, Voronoi } from "@packages/victory-voronoi"; import { ErrorBar, VictoryErrorBar } from "@packages/victory-errorbar"; import { Candle, VictoryCandlestick } from "@packages/victory-candlestick"; -import { LineSegment, Whisker, Border, Point, VictoryLabel } from "@packages/victory-core"; +import { + LineSegment, + Whisker, + Border, + Point, + VictoryLabel, + VictoryAccessibleGroup +} from "@packages/victory-core"; import { accessibilityBarData, accessibilityBoxData, accessibilityPieData, - accessibilityLineData, accessibilityAreaData, + accessibilityLineData, + accessibilityGroupData, accessibilityScatterData, accessibilityVoronoiData, accessibilityErrorBarData, @@ -35,7 +44,8 @@ const pageHeadingStyle: React.CSSProperties = { const chartHeadingStyle: React.CSSProperties = { marginBottom: "0px", - marginTop: "25px" + marginTop: "25px", + fontSize: "calc(1vw + 5px)" }; const containerStyle: React.CSSProperties = { @@ -50,7 +60,8 @@ const chartContainerStyle: React.CSSProperties = { flexDirection: "column", alignItems: "center", width: "50%", - height: "50%" + height: "50%", + padding: "25px" }; export const assignIndexValue = (index: number | string, value: number): number => { @@ -66,6 +77,7 @@ export default class VictoryAccessibilityDemo extends React.Component {

Tabbable charts with aria-labels

+ {/**BAR */}

Bar chart

@@ -81,6 +93,8 @@ export default class VictoryAccessibilityDemo extends React.Component { />
+ + {/** BOXPLOT */}

BoxPlot

@@ -137,10 +151,20 @@ export default class VictoryAccessibilityDemo extends React.Component { {/** AREA */}

Area

- - + + + } + > `area chart stack ${data[0]._stack}`} @@ -149,6 +173,7 @@ export default class VictoryAccessibilityDemo extends React.Component { } /> { /> `area chart stack ${data[0]._stack}`} @@ -168,6 +194,7 @@ export default class VictoryAccessibilityDemo extends React.Component { /> `area chart stack ${data[0]._stack}`} @@ -299,6 +326,38 @@ export default class VictoryAccessibilityDemo extends React.Component { />
+ + {/**ACCESSIBLE GROUP */} +
+

Accessible Group

+ + + } + > + + + } + /> + + + +
); diff --git a/packages/victory-core/src/index.d.ts b/packages/victory-core/src/index.d.ts index 3e647634c..0e237203d 100644 --- a/packages/victory-core/src/index.d.ts +++ b/packages/victory-core/src/index.d.ts @@ -271,6 +271,7 @@ export class VictoryContainer extends React.Component {} + +// #endregion + // #region Victory Theme export type ThemeBaseProps = { diff --git a/packages/victory-core/src/index.js b/packages/victory-core/src/index.js index 3e5f4f3b8..ce81cb6a8 100644 --- a/packages/victory-core/src/index.js +++ b/packages/victory-core/src/index.js @@ -1,3 +1,4 @@ +export { default as VictoryAccessibleGroup } from "./victory-accessible-group/victory-accessible-group"; export { default as VictoryAnimation } from "./victory-animation/victory-animation"; export { default as VictoryContainer } from "./victory-container/victory-container"; export { default as VictoryLabel } from "./victory-label/victory-label"; diff --git a/packages/victory-core/src/victory-accessible-group/victory-accessible-group.js b/packages/victory-core/src/victory-accessible-group/victory-accessible-group.js new file mode 100644 index 000000000..7e2147259 --- /dev/null +++ b/packages/victory-core/src/victory-accessible-group/victory-accessible-group.js @@ -0,0 +1,46 @@ +import React from "react"; +import PropTypes from "prop-types"; + +class VictoryAccessibleGroup extends React.Component { + static displayName = "VictoryAccessibleGroup"; + static propTypes = { + "aria-describedby": PropTypes.string, + "aria-label": PropTypes.string, + desc: PropTypes.string, + children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]), + className: PropTypes.string, + tabIndex: PropTypes.number + }; + + static defaultProps = { + className: "VictoryAccessibleGroup" + }; + + render() { + const { desc, children, className, tabIndex } = this.props; + const descId = desc && (this.props["aria-describedby"] || desc.split(" ").join("-")); + + return desc ? ( + + {desc} + {children} + + ) : ( + + {children} + + ); + } +} + +export default VictoryAccessibleGroup; diff --git a/packages/victory-core/src/victory-clip-container/victory-clip-container.js b/packages/victory-core/src/victory-clip-container/victory-clip-container.js index d6b46b263..29b667147 100644 --- a/packages/victory-core/src/victory-clip-container/victory-clip-container.js +++ b/packages/victory-core/src/victory-clip-container/victory-clip-container.js @@ -11,6 +11,7 @@ export default class VictoryClipContainer extends React.Component { static displayName = "VictoryClipContainer"; static role = "container"; static propTypes = { + "aria-label": PropTypes.string, children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]), circleComponent: PropTypes.element, className: PropTypes.string, @@ -30,6 +31,7 @@ export default class VictoryClipContainer extends React.Component { polar: PropTypes.bool, radius: CustomPropTypes.nonNegative, style: PropTypes.object, + tabIndex: PropTypes.number, transform: PropTypes.string, translateX: PropTypes.number, translateY: PropTypes.number @@ -61,7 +63,7 @@ export default class VictoryClipContainer extends React.Component { } renderClippedGroup(props, clipId) { - const { style, events, transform, children, className, groupComponent } = props; + const { style, events, transform, children, className, groupComponent, tabIndex } = props; const clipComponent = this.renderClipComponent(props, clipId); const groupProps = assign( { @@ -73,17 +75,18 @@ export default class VictoryClipContainer extends React.Component { }, events ); - return React.cloneElement(groupComponent, groupProps, [ - clipComponent, - ...React.Children.toArray(children) - ]); + return React.cloneElement( + groupComponent, + { ...groupProps, "aria-label": props["aria-label"], tabIndex }, + [clipComponent, ...React.Children.toArray(children)] + ); } renderGroup(props) { - const { style, events, transform, children, className, groupComponent } = props; + const { style, events, transform, children, className, groupComponent, tabIndex } = props; return React.cloneElement( groupComponent, - assign({ className, style, transform }, events), + assign({ className, style, transform, "aria-label": props["aria-label"], tabIndex }, events), children ); } diff --git a/test/client/spec/victory-core/victory-accessible-group/victory-accessible-group.spec.js b/test/client/spec/victory-core/victory-accessible-group/victory-accessible-group.spec.js new file mode 100644 index 000000000..01cb9dc46 --- /dev/null +++ b/test/client/spec/victory-core/victory-accessible-group/victory-accessible-group.spec.js @@ -0,0 +1,50 @@ +/** + * Client tests + */ +/* global sinon */ +/*eslint-disable max-nested-callbacks */ +/* eslint no-unused-expressions: 0 */ +import React from "react"; +import { shallow, mount } from "enzyme"; +import VictoryAccessibleGroup from "packages/victory-core/src/victory-accessible-group/victory-accessible-group"; + +describe("components/victory-accessible-group", () => { + it("renders an g with an aria-label", () => { + const wrapper = shallow(); + expect(wrapper.find("g")).to.have.length(1); + expect(wrapper.find("g").prop("aria-label")).to.equal("test-aria-label"); + }); + + it("renders an g with a tabIndex and className", () => { + const wrapper = shallow(); + expect(wrapper.find("g").prop("tabIndex")).to.equal(5); + expect(wrapper.find("g").prop("className")).to.equal("accessibility"); + }); + + it("renders an g with a desc node if given", () => { + const wrapper = shallow( + + ); + expect(wrapper.find("g").prop("aria-describedby")).to.equal("describes group"); + expect(wrapper.find("desc").text()).to.equal("test description"); + expect(wrapper.find("desc").props().id).to.equal("describes group"); + }); + + it("uses the desc prop value for descId and aria-describedby if no aria-describedby prop value", () => { + const wrapper = shallow( + + ); + expect(wrapper.find("desc").text()).to.equal("applies to both aria-describeby and descId"); + expect(wrapper.find("g").prop("aria-describedby")).to.equal( + "applies-to-both-aria-describeby-and-descId" + ); + expect(wrapper.find("desc").props().id).to.equal("applies-to-both-aria-describeby-and-descId"); + }); +});