diff --git a/demo/ts/app.tsx b/demo/ts/app.tsx index d3c80c67a..a402d0b74 100644 --- a/demo/ts/app.tsx +++ b/demo/ts/app.tsx @@ -27,6 +27,7 @@ import PolarAxisDemo from "./components/victory-polar-axis-demo"; import PrimitivesDemo from "./components/primitives-demo"; import ScatterDemo from "./components/victory-scatter-demo"; import SelectionDemo from "./components/selection-demo"; +import StackDemo from "./components/victory-stack-demo"; import TooltipDemo from "./components/victory-tooltip-demo"; import VictoryDemo from "./components/victory-demo"; import VictorySelectionContainerDemo from "./components/victory-selection-container-demo"; @@ -68,6 +69,7 @@ const MAP = { "/primitives": { component: PrimitivesDemo, name: "PrimitivesDemo" }, "/scatter": { component: ScatterDemo, name: "ScatterDemo" }, "/selection": { component: SelectionDemo, name: "SelectionDemo" }, + "/stack": { component: StackDemo, name: "StackDemo" }, "/tooltip": { component: TooltipDemo, name: "TooltipDemo" }, "/victory-demo": { component: VictoryDemo, name: "VictoryDemo" }, "/victory-selection-container": { diff --git a/demo/ts/components/group-demo.tsx b/demo/ts/components/group-demo.tsx index 7378e3107..62993c40b 100644 --- a/demo/ts/components/group-demo.tsx +++ b/demo/ts/components/group-demo.tsx @@ -64,6 +64,8 @@ class App extends React.Component { colorScale="qualitative" > + + Math.tan(2 * Math.PI * data.x)} + > + + } + labels={({ datum }) => datum.y} + /> + ); diff --git a/demo/ts/components/victory-chart-demo.tsx b/demo/ts/components/victory-chart-demo.tsx index 6a98ec235..1c0ba0e82 100644 --- a/demo/ts/components/victory-chart-demo.tsx +++ b/demo/ts/components/victory-chart-demo.tsx @@ -236,11 +236,21 @@ class VictoryChartDemo extends React.Component {

VictoryChart

- + - + { - + +

Standalone Stack

+ + + + + +
+ ) + } +} + +export default App; \ No newline at end of file diff --git a/packages/victory-chart/src/index.d.ts b/packages/victory-chart/src/index.d.ts index 03573ee63..123be7e52 100644 --- a/packages/victory-chart/src/index.d.ts +++ b/packages/victory-chart/src/index.d.ts @@ -18,6 +18,7 @@ export interface VictoryChartProps extends VictoryCommonProps { backgroundComponent?: React.ReactElement; categories?: CategoryPropType; children?: React.ReactNode | React.ReactNode[]; + desc?: string; defaultAxes?: AxesType; defaultPolarAxes?: AxesType; domain?: DomainPropType; @@ -33,6 +34,7 @@ export interface VictoryChartProps extends VictoryCommonProps { style?: Pick & { background?: VictoryStyleObject; }; + title?: string; } export class VictoryChart extends React.Component {} diff --git a/packages/victory-chart/src/victory-chart.js b/packages/victory-chart/src/victory-chart.js index 1592b4580..2bdf17adf 100644 --- a/packages/victory-chart/src/victory-chart.js +++ b/packages/victory-chart/src/victory-chart.js @@ -3,13 +3,14 @@ import PropTypes from "prop-types"; import React from "react"; import { Background, + CommonProps, Helpers, + Hooks, + PropTypes as CustomPropTypes, + UserProps, VictoryContainer, VictoryTheme, - CommonProps, - PropTypes as CustomPropTypes, Wrapper, - Hooks } from "victory-core"; import { VictorySharedEvents } from "victory-shared-events"; import { VictoryAxis } from "victory-axis"; @@ -124,12 +125,13 @@ const VictoryChart = (initialProps) => { const defaultContainerProps = defaults( {}, containerComponent.props, - containerProps + containerProps, + UserProps.getSafeUserProps(initialProps) ); return React.cloneElement(containerComponent, defaultContainerProps); } return groupComponent; - }, [groupComponent, standalone, containerComponent, containerProps]); + }, [groupComponent, standalone, containerComponent, containerProps, initialProps]); const events = React.useMemo(() => { return Wrapper.getAllEvents(props); diff --git a/packages/victory-core/src/index.js b/packages/victory-core/src/index.js index 0c923ea46..d486de502 100644 --- a/packages/victory-core/src/index.js +++ b/packages/victory-core/src/index.js @@ -39,6 +39,7 @@ export * as Style from "./victory-util/style"; export * as TextSize from "./victory-util/textsize"; export { default as Timer } from "./victory-util/timer"; export * as Transitions from "./victory-util/transitions"; +export * as UserProps from "./victory-util/user-props"; export * as CommonProps from "./victory-util/common-props"; export * as Wrapper from "./victory-util/wrapper"; export * as Axis from "./victory-util/axis"; diff --git a/packages/victory-core/src/victory-container/victory-container.js b/packages/victory-core/src/victory-container/victory-container.js index f51c3f6e3..3470922a4 100644 --- a/packages/victory-core/src/victory-container/victory-container.js +++ b/packages/victory-core/src/victory-container/victory-container.js @@ -6,6 +6,7 @@ import Portal from "../victory-portal/portal"; import PortalContext from "../victory-portal/portal-context"; import TimerContext from "../victory-util/timer-context"; import * as Helpers from "../victory-util/helpers"; +import * as UserProps from "../victory-util/user-props"; export default class VictoryContainer extends React.Component { static displayName = "VictoryContainer"; @@ -192,9 +193,13 @@ export default class VictoryContainer extends React.Component { preserveAspectRatio, role } = this.props; + const style = responsive ? this.props.style : Helpers.omit(this.props.style, ["height", "width"]); + + const userProps = UserProps.getSafeUserProps(this.props); + const svgProps = assign( { width, @@ -213,7 +218,8 @@ export default class VictoryContainer extends React.Component { .filter(Boolean) .join(" ") || undefined, viewBox: responsive ? `0 0 ${width} ${height}` : undefined, - preserveAspectRatio: responsive ? preserveAspectRatio : undefined + preserveAspectRatio: responsive ? preserveAspectRatio : undefined, + ...userProps }, events ); diff --git a/packages/victory-core/src/victory-util/add-events.js b/packages/victory-core/src/victory-util/add-events.js index 76b293679..71c0e765c 100644 --- a/packages/victory-core/src/victory-util/add-events.js +++ b/packages/victory-core/src/victory-util/add-events.js @@ -14,6 +14,7 @@ import { import * as Events from "./events"; import isEqual from "react-fast-compare"; import VictoryTransition from "../victory-transition/victory-transition"; +import * as UserProps from '../victory-util/user-props'; const datumHasXandY = (datum) => { return !isNil(datum._x) && !isNil(datum._y); @@ -274,11 +275,14 @@ export default (WrappedComponent, options) => { const parentProps = isContainer ? this.getComponentProps(component, "parent", "parent") : {}; + if (parentProps.events) { this.globalEvents = Events.getGlobalEvents(parentProps.events); parentProps.events = Events.omitGlobalEvents(parentProps.events); } - return React.cloneElement(component, parentProps, children); + + const componentProps = { ...parentProps, ...parentProps.userProps }; + return React.cloneElement(component, componentProps, children); } animateComponent(props, defaultAnimationWhitelist) { @@ -327,23 +331,24 @@ export default (WrappedComponent, options) => { renderData(props, shouldRenderDatum = datumHasXandY) { const { dataComponent, labelComponent, groupComponent } = props; - + const userProps = UserProps.getSafeUserProps(props); + const dataComponents = this.dataKeys.reduce( (validDataComponents, _dataKey, index) => { const dataProps = this.getComponentProps( dataComponent, "data", index - ); - if (shouldRenderDatum(dataProps.datum)) { - validDataComponents.push( - React.cloneElement(dataComponent, dataProps) ); - } - return validDataComponents; - }, - [] - ); + if (shouldRenderDatum(dataProps.datum)) { + validDataComponents.push( + React.cloneElement(dataComponent, dataProps) + ); + } + return validDataComponents; + }, + [] + ); const labelComponents = this.dataKeys .map((_dataKey, index) => { @@ -360,7 +365,8 @@ export default (WrappedComponent, options) => { .filter(Boolean); const children = [...dataComponents, ...labelComponents]; - return this.renderContainer(groupComponent, children); + const group = React.cloneElement(groupComponent, { ...userProps }, children); + return this.renderContainer(group, children); } }; }; diff --git a/packages/victory-core/src/victory-util/user-props.js b/packages/victory-core/src/victory-util/user-props.js new file mode 100644 index 000000000..88504cbd6 --- /dev/null +++ b/packages/victory-core/src/victory-util/user-props.js @@ -0,0 +1,64 @@ +/* + USER_PROPS_SAFELIST is to contain any string deemed safe for user props. + The startsWidth array will contain the start of any accepted user-prop that + starts with these characters. + The exactMatch will contain a list of exact prop names that are accepted. +*/ +const USER_PROPS_SAFELIST = { + startsWith: ["data-", "aria-"], + exactMatch: [] +}; + +/** + * doesPropStartWith: Function that takes a prop's key and runs it against all + * options in the USER_PROPS_SAFELIST and checks to see if it starts with any + * of those options. + * @param {string} key: prop key to be tested against whitelist + * @returns {Boolean}: returns true if the key starts with an option or false if + * otherwise + */ +const doesPropStartWith = (key) => { + let startsWith = false; + + USER_PROPS_SAFELIST.startsWith.forEach((starterString) => { + const regex = new RegExp(`\\b(${starterString})(\\w|-)+`, "g"); + if (regex.test(key)) startsWith = true; + }); + + return startsWith; +}; + +/** + * isExactMatch: checks to see if the given key matches any of the 'exactMatch' + * items in the whitelist + * @param {String} key: prop key to be tested against the whitelist-exact match + * array. + * @returns {Boolean}: return true if whitelist contains that key, otherwise + * returns false. + */ +const isExactMatch = (key) => USER_PROPS_SAFELIST.exactMatch.includes(key); + +/** + * testIfSafeProp: tests prop's key against both startsWith and exactMatch values + * @param {String} key: prop key to be tested against the whitelist + * @returns {Boolean}: returns true if found in whitelist, otherwise returns false + */ +const testIfSafeProp = (key) => { + if (doesPropStartWith(key) || isExactMatch(key)) return true; + return false; +}; + +/** + * getSafeUserProps - function that takes in a props object and removes any + * key-value entries that do not match filter strings in the USER_PROPS_SAFELIST + * object. + * + * @param {Object} props: props to be filtered against USER_PROPS_SAFELIST + * @returns {Object}: object containing remaining acceptable props + */ +export const getSafeUserProps = (props) => { + const propsToFilter = { ...props }; + return Object.fromEntries( + Object.entries(propsToFilter).filter(([key]) => testIfSafeProp(key)) + ); +}; \ No newline at end of file diff --git a/packages/victory-group/src/victory-group.js b/packages/victory-group/src/victory-group.js index f3a70ee92..b428f88f8 100644 --- a/packages/victory-group/src/victory-group.js +++ b/packages/victory-group/src/victory-group.js @@ -2,12 +2,13 @@ import { assign, defaults, isEmpty } from "lodash"; import PropTypes from "prop-types"; import React from "react"; import { + CommonProps, Helpers, + Hooks, + UserProps, VictoryContainer, VictoryTheme, - CommonProps, Wrapper, - Hooks } from "victory-core"; import { VictorySharedEvents } from "victory-shared-events"; import { getChildren, useMemoizedProps } from "./helper-methods"; @@ -88,17 +89,21 @@ const VictoryGroup = (initialProps) => { name ]); + const userProps = React.useMemo(() => UserProps.getSafeUserProps(initialProps), [initialProps]); + const container = React.useMemo(() => { if (standalone) { const defaultContainerProps = defaults( {}, containerComponent.props, - containerProps + containerProps, + userProps ); return React.cloneElement(containerComponent, defaultContainerProps); } - return groupComponent; - }, [groupComponent, standalone, containerComponent, containerProps]); + + return React.cloneElement(groupComponent, userProps); + }, [groupComponent, standalone, containerComponent, containerProps, userProps]); const events = React.useMemo(() => { return Wrapper.getAllEvents(props); diff --git a/packages/victory-stack/src/victory-stack.js b/packages/victory-stack/src/victory-stack.js index 886beab46..84ebf6506 100644 --- a/packages/victory-stack/src/victory-stack.js +++ b/packages/victory-stack/src/victory-stack.js @@ -2,13 +2,14 @@ import { assign, defaults, isEmpty } from "lodash"; import PropTypes from "prop-types"; import React from "react"; import { + CommonProps, Helpers, + Hooks, + PropTypes as CustomPropTypes, + UserProps, VictoryContainer, VictoryTheme, - CommonProps, Wrapper, - PropTypes as CustomPropTypes, - Hooks } from "victory-core"; import { VictorySharedEvents } from "victory-shared-events"; import { getChildren, useMemoizedProps } from "./helper-methods"; @@ -94,18 +95,20 @@ const VictoryStack = (initialProps) => { origin, name ]); + const userProps = React.useMemo(() => UserProps.getSafeUserProps(initialProps), [initialProps]); const container = React.useMemo(() => { if (standalone) { const defaultContainerProps = defaults( {}, containerComponent.props, - containerProps + containerProps, + userProps ); return React.cloneElement(containerComponent, defaultContainerProps); } - return groupComponent; - }, [groupComponent, standalone, containerComponent, containerProps]); + return React.cloneElement(groupComponent, userProps); + }, [groupComponent, standalone, containerComponent, containerProps, userProps]); const events = React.useMemo(() => { return Wrapper.getAllEvents(props); diff --git a/packages/victory/src/index.js b/packages/victory/src/index.js index 5f4fb2bc6..b578275b7 100644 --- a/packages/victory/src/index.js +++ b/packages/victory/src/index.js @@ -1,43 +1,44 @@ export { + addEvents, + Axis, Background, Border, Box, - ClipPath, - LineSegment, - Whisker, Circle, - Rect, - Line, - Path, - TSpan, - Text, - Point, - VictoryAnimation, - VictoryContainer, - VictoryLabel, - VictoryTheme, - VictoryTransition, - VictoryPortal, - Portal, - VictoryClipContainer, - addEvents, + ClipPath, Collection, Data, DefaultTransitions, Domain, Events, Helpers, + LabelHelpers, + Line, + LineSegment, Log, + Path, + Point, + Portal, PropTypes, + Rect, Scale, + Selection, Style, + Text, TextSize, Transitions, - Selection, - LabelHelpers, - Axis, + TSpan, + UserProps, + VictoryAccessibleGroup, + VictoryAnimation, + VictoryClipContainer, + VictoryContainer, + VictoryLabel, + VictoryPortal, + VictoryTheme, + VictoryTransition, + Whisker, Wrapper, - VictoryAccessibleGroup } from "victory-core"; export { VictoryChart } from "victory-chart";