diff --git a/packages/victory-core/src/victory-util/add-events.tsx b/packages/victory-core/src/victory-util/add-events.tsx index 40cece210..f67a07d1f 100644 --- a/packages/victory-core/src/victory-util/add-events.tsx +++ b/packages/victory-core/src/victory-util/add-events.tsx @@ -68,9 +68,9 @@ export interface EventsMixinClass { defaultAnimationWhitelist: string[], ): React.ReactElement; getComponentProps( - component: React.ReactElement, + component: React.ReactNode, type: string, - index: number, + index: string | number, ): TProps; dataKeys: string[]; } @@ -352,7 +352,11 @@ export function addEvents< return props.events; } - getComponentProps(component, type, index) { + getComponentProps( + component: React.ReactNode, + type: string, + index: string | number, + ) { const name = this.props.name || WrappedComponent.role; const key = (this.dataKeys && this.dataKeys[index]) || index; const id = `${name}-${type}-${key}`; @@ -365,13 +369,18 @@ export function addEvents< return undefined; } + const currentProps = + component && typeof component === "object" && "props" in component + ? component.props + : undefined; + if (this.hasEvents) { const baseEvents = this.getEvents(this.props, type, key); const componentProps = defaults( { index, key: id }, this.getEventState(key, type), this.getSharedEventState(key, type), - component.props, + currentProps, baseProps, { id }, ); @@ -385,7 +394,7 @@ export function addEvents< return assign({}, componentProps, { events }); } - return defaults({ index, key: id }, component.props, baseProps, { id }); + return defaults({ index, key: id }, currentProps, baseProps, { id }); } renderContainer(component, children) { diff --git a/packages/victory-legend/package.json b/packages/victory-legend/package.json index 6c88c6f57..86ae99e50 100644 --- a/packages/victory-legend/package.json +++ b/packages/victory-legend/package.json @@ -27,6 +27,9 @@ "peerDependencies": { "react": ">=16.6.0" }, + "devDependencies": { + "victory-legend": "*" + }, "publishConfig": { "provenance": true }, diff --git a/packages/victory-legend/src/helper-methods.js b/packages/victory-legend/src/helper-methods.ts similarity index 98% rename from packages/victory-legend/src/helper-methods.js rename to packages/victory-legend/src/helper-methods.ts index 98d92ab5a..1747edaf3 100644 --- a/packages/victory-legend/src/helper-methods.js +++ b/packages/victory-legend/src/helper-methods.ts @@ -86,7 +86,7 @@ const getColumnWidths = (props, data) => { : gutter || 0; const dataByColumn = groupBy(data, "column"); const columns = keys(dataByColumn); - return columns.reduce((memo, curr, index) => { + return columns.reduce((memo, curr, index) => { const lengths = dataByColumn[curr].map((d) => { return d.textSize.width + d.size + d.symbolSpacer + gutterWidth; }); @@ -102,7 +102,7 @@ const getRowHeights = (props, data) => { ? (gutter.top || 0) + (gutter.bottom || 0) : gutter || 0; const dataByRow = groupBy(data, "row"); - return keys(dataByRow).reduce((memo, curr, index) => { + return keys(dataByRow).reduce((memo, curr, index) => { const rows = dataByRow[curr]; const lengths = rows.map((d) => { return d.textSize.height + d.symbolSpacer + gutterHeight; diff --git a/packages/victory-legend/src/index.d.ts b/packages/victory-legend/src/index.d.ts deleted file mode 100644 index 66f048a88..000000000 --- a/packages/victory-legend/src/index.d.ts +++ /dev/null @@ -1,59 +0,0 @@ -import * as React from "react"; -import { - BlockProps, - ColorScalePropType, - EventPropTypeInterface, - OrientationTypes, - PaddingProps, - StringOrNumberOrCallback, - VictoryCommonProps, - VictoryDatableProps, - VictorySingleLabelableProps, - VictoryStyleInterface, - VictoryLabelStyleObject, -} from "victory-core"; - -export type VictoryLegendTTargetType = "data" | "labels" | "parent"; -export type VictoryLegendOrientationType = "horizontal" | "vertical"; - -export interface VictoryLegendProps - extends VictoryCommonProps, - VictoryDatableProps, - VictorySingleLabelableProps { - borderComponent?: React.ReactElement; - borderPadding?: PaddingProps; - centerTitle?: boolean; - colorScale?: ColorScalePropType; - data?: Array<{ - name?: string; - labels?: { - fill?: string; - }; - symbol?: { - fill?: string; - size?: number; - type?: string; - }; - }>; - dataComponent?: React.ReactElement; - eventKey?: StringOrNumberOrCallback | string[]; - events?: EventPropTypeInterface< - VictoryLegendTTargetType, - StringOrNumberOrCallback - >[]; - gutter?: number | { left: number; right: number }; - itemsPerRow?: number; - orientation?: VictoryLegendOrientationType; - rowGutter?: number | Omit; - style?: VictoryStyleInterface & { - title?: VictoryLabelStyleObject | VictoryLabelStyleObject[]; - }; - symbolSpacer?: number; - title?: string | string[]; - titleComponent?: React.ReactElement; - titleOrientation?: OrientationTypes; - x?: number; - y?: number; -} - -export class VictoryLegend extends React.Component {} diff --git a/packages/victory-legend/src/index.js b/packages/victory-legend/src/index.js deleted file mode 100644 index 33893adcf..000000000 --- a/packages/victory-legend/src/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as VictoryLegend } from "./victory-legend"; diff --git a/packages/victory-legend/src/index.ts b/packages/victory-legend/src/index.ts new file mode 100644 index 000000000..04641851b --- /dev/null +++ b/packages/victory-legend/src/index.ts @@ -0,0 +1 @@ +export * from "./victory-legend"; diff --git a/packages/victory-legend/src/victory-legend.js b/packages/victory-legend/src/victory-legend.js deleted file mode 100644 index 0a0b94bac..000000000 --- a/packages/victory-legend/src/victory-legend.js +++ /dev/null @@ -1,240 +0,0 @@ -import React from "react"; -import PropTypes from "prop-types"; -import { getBaseProps, getDimensions } from "./helper-methods"; -import { - PropTypes as CustomPropTypes, - addEvents, - Helpers, - VictoryLabel, - VictoryContainer, - VictoryTheme, - Point, - Border, -} from "victory-core"; - -const fallbackProps = { - orientation: "vertical", - titleOrientation: "top", - width: 450, - height: 300, - x: 0, - y: 0, -}; - -const defaultLegendData = [{ name: "Series 1" }, { name: "Series 2" }]; - -class VictoryLegend extends React.Component { - static displayName = "VictoryLegend"; - - static role = "legend"; - - static propTypes = { - borderComponent: PropTypes.element, - borderPadding: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.shape({ - top: PropTypes.number, - bottom: PropTypes.number, - left: PropTypes.number, - right: PropTypes.number, - }), - ]), - centerTitle: PropTypes.bool, - colorScale: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.string), - PropTypes.oneOf([ - "grayscale", - "qualitative", - "heatmap", - "warm", - "cool", - "red", - "green", - "blue", - ]), - ]), - containerComponent: PropTypes.element, - data: PropTypes.arrayOf( - PropTypes.shape({ - name: PropTypes.string.isRequired, - label: PropTypes.object, - symbol: PropTypes.object, - }), - ), - dataComponent: PropTypes.element, - eventKey: PropTypes.oneOfType([ - PropTypes.func, - CustomPropTypes.allOfType([ - CustomPropTypes.integer, - CustomPropTypes.nonNegative, - ]), - PropTypes.string, - ]), - events: PropTypes.arrayOf( - PropTypes.shape({ - target: PropTypes.oneOf(["data", "labels", "parent"]), - eventKey: PropTypes.oneOfType([ - PropTypes.array, - CustomPropTypes.allOfType([ - CustomPropTypes.integer, - CustomPropTypes.nonNegative, - ]), - PropTypes.string, - ]), - eventHandlers: PropTypes.object, - }), - ), - externalEventMutations: PropTypes.arrayOf( - PropTypes.shape({ - callback: PropTypes.func, - childName: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), - eventKey: PropTypes.oneOfType([ - PropTypes.array, - CustomPropTypes.allOfType([ - CustomPropTypes.integer, - CustomPropTypes.nonNegative, - ]), - PropTypes.string, - ]), - mutation: PropTypes.func, - target: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), - }), - ), - groupComponent: PropTypes.element, - gutter: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.shape({ - left: PropTypes.number, - right: PropTypes.number, - }), - ]), - height: CustomPropTypes.nonNegative, - itemsPerRow: CustomPropTypes.nonNegative, - labelComponent: PropTypes.element, - name: PropTypes.string, - orientation: PropTypes.oneOf(["horizontal", "vertical"]), - padding: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.shape({ - top: PropTypes.number, - bottom: PropTypes.number, - left: PropTypes.number, - right: PropTypes.number, - }), - ]), - rowGutter: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.shape({ - top: PropTypes.number, - bottom: PropTypes.number, - }), - ]), - sharedEvents: PropTypes.shape({ - events: PropTypes.array, - getEventState: PropTypes.func, - }), - standalone: PropTypes.bool, - style: PropTypes.shape({ - border: PropTypes.object, - data: PropTypes.object, - labels: PropTypes.object, - parent: PropTypes.object, - title: PropTypes.object, - }), - symbolSpacer: PropTypes.number, - theme: PropTypes.object, - title: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), - titleComponent: PropTypes.element, - titleOrientation: PropTypes.oneOf(["top", "bottom", "left", "right"]), - width: CustomPropTypes.nonNegative, - x: CustomPropTypes.nonNegative, - y: CustomPropTypes.nonNegative, - }; - - static defaultProps = { - borderComponent: , - data: defaultLegendData, - containerComponent: , - dataComponent: , - groupComponent: , - labelComponent: , - standalone: true, - theme: VictoryTheme.grayscale, - titleComponent: , - }; - - static getBaseProps = (props) => getBaseProps(props, fallbackProps); - static getDimensions = (props) => getDimensions(props, fallbackProps); - static expectedComponents = [ - "borderComponent", - "containerComponent", - "dataComponent", - "groupComponent", - "labelComponent", - "titleComponent", - ]; - - renderChildren(props) { - const { dataComponent, labelComponent, title } = props; - const dataComponents = this.dataKeys - .map((_dataKey, index) => { - if (_dataKey === "all") { - return undefined; - } - const dataProps = this.getComponentProps(dataComponent, "data", index); - return React.cloneElement(dataComponent, dataProps); - }) - .filter(Boolean); - - const labelComponents = this.dataKeys - .map((_dataKey, index) => { - if (_dataKey === "all") { - return undefined; - } - const labelProps = this.getComponentProps( - labelComponent, - "labels", - index, - ); - if (labelProps.text !== undefined && labelProps.text !== null) { - return React.cloneElement(labelComponent, labelProps); - } - return undefined; - }) - .filter(Boolean); - const borderProps = this.getComponentProps( - props.borderComponent, - "border", - "all", - ); - const borderComponent = React.cloneElement( - props.borderComponent, - borderProps, - ); - if (title) { - const titleProps = this.getComponentProps(props.title, "title", "all"); - const titleComponent = React.cloneElement( - props.titleComponent, - titleProps, - ); - return [ - borderComponent, - ...dataComponents, - titleComponent, - ...labelComponents, - ]; - } - return [borderComponent, ...dataComponents, ...labelComponents]; - } - - render() { - const { role } = this.constructor; - const props = Helpers.modifyProps(this.props, fallbackProps, role); - const children = [this.renderChildren(props)]; - return props.standalone - ? this.renderContainer(props.containerComponent, children) - : React.cloneElement(props.groupComponent, {}, children); - } -} - -export default addEvents(VictoryLegend); diff --git a/packages/victory-legend/src/victory-legend.test.js b/packages/victory-legend/src/victory-legend.test.tsx similarity index 100% rename from packages/victory-legend/src/victory-legend.test.js rename to packages/victory-legend/src/victory-legend.test.tsx diff --git a/packages/victory-legend/src/victory-legend.tsx b/packages/victory-legend/src/victory-legend.tsx new file mode 100644 index 000000000..6b9b97974 --- /dev/null +++ b/packages/victory-legend/src/victory-legend.tsx @@ -0,0 +1,194 @@ +import React from "react"; +import { getBaseProps, getDimensions } from "./helper-methods"; +import { + addEvents, + Helpers, + VictoryLabel, + VictoryContainer, + VictoryTheme, + Point, + Border, + BlockProps, + ColorScalePropType, + EventPropTypeInterface, + OrientationTypes, + PaddingProps, + StringOrNumberOrCallback, + VictoryCommonProps, + VictoryDatableProps, + VictorySingleLabelableProps, + VictoryStyleInterface, + VictoryLabelStyleObject, + EventsMixinClass, +} from "victory-core"; + +export type VictoryLegendTTargetType = "data" | "labels" | "parent"; + +export type VictoryLegendOrientationType = "horizontal" | "vertical"; + +export interface VictoryLegendProps + extends VictoryCommonProps, + VictoryDatableProps, + VictorySingleLabelableProps { + borderComponent?: React.ReactElement; + borderPadding?: PaddingProps; + centerTitle?: boolean; + colorScale?: ColorScalePropType; + dataComponent?: React.ReactElement; + eventKey?: StringOrNumberOrCallback | string[]; + events?: EventPropTypeInterface< + VictoryLegendTTargetType, + StringOrNumberOrCallback + >[]; + gutter?: number | { left: number; right: number }; + itemsPerRow?: number; + orientation?: VictoryLegendOrientationType; + rowGutter?: number | Omit; + style?: VictoryStyleInterface & { + title?: VictoryLabelStyleObject | VictoryLabelStyleObject[]; + }; + symbolSpacer?: number; + title?: string | string[]; + titleComponent?: React.ReactElement; + titleOrientation?: OrientationTypes; +} + +const fallbackProps = { + orientation: "vertical", + titleOrientation: "top", + width: 450, + height: 300, + x: 0, + y: 0, +}; + +const defaultLegendData = [{ name: "Series 1" }, { name: "Series 2" }]; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface VictoryLegendBase extends EventsMixinClass {} + +class VictoryLegendBase extends React.Component { + static displayName = "VictoryLegend"; + + static role = "legend"; + + static defaultProps = { + borderComponent: , + data: defaultLegendData, + containerComponent: , + dataComponent: , + groupComponent: , + labelComponent: , + standalone: true, + theme: VictoryTheme.grayscale, + titleComponent: , + }; + + static getBaseProps = (props: VictoryLegendProps) => + getBaseProps(props, fallbackProps); + + static getDimensions = (props: VictoryLegendProps) => + getDimensions(props, fallbackProps); + + static expectedComponents = [ + "borderComponent", + "containerComponent", + "dataComponent", + "groupComponent", + "labelComponent", + "titleComponent", + ]; + + renderChildren(props: VictoryLegendProps) { + const { dataComponent, labelComponent, title } = props; + + const children: React.ReactElement[] = []; + + if (props.borderComponent) { + const borderProps = this.getComponentProps( + props.borderComponent, + "border", + "all", + ); + const borderComponent = React.cloneElement( + props.borderComponent, + borderProps, + ); + + children.push(borderComponent); + } + + if (dataComponent) { + const dataComponents = this.dataKeys + .map((_dataKey, index) => { + if (_dataKey === "all") { + return undefined; + } + const dataProps = this.getComponentProps( + dataComponent, + "data", + index, + ); + return React.cloneElement(dataComponent, dataProps); + }) + .filter( + (comp: React.ReactElement | undefined): comp is React.ReactElement => + comp !== undefined, + ); + + children.push(...dataComponents); + } + + if (title && props.titleComponent) { + const titleProps = this.getComponentProps(title, "title", "all"); + const titleComponent = React.cloneElement( + props.titleComponent, + titleProps, + ); + + children.push(titleComponent); + } + + if (labelComponent) { + const labelComponents = this.dataKeys + .map((_dataKey, index) => { + if (_dataKey === "all") { + return undefined; + } + const labelProps = this.getComponentProps( + labelComponent, + "labels", + index, + ); + if ( + (labelProps as any).text !== undefined && + (labelProps as any).text !== null + ) { + return React.cloneElement(labelComponent, labelProps); + } + return undefined; + }) + .filter( + (comp: React.ReactElement | undefined): comp is React.ReactElement => + comp !== undefined, + ); + + children.push(...labelComponents); + } + + return children; + } + + render(): React.ReactElement { + // @ts-expect-error Property 'role' does not exist on type 'Function'. + // Ref: https://github.com/microsoft/TypeScript/issues/32452 + const { role } = this.constructor; + const props = Helpers.modifyProps(this.props, fallbackProps, role); + const children = this.renderChildren(props); + return props.standalone + ? this.renderContainer(props.containerComponent, children) + : React.cloneElement(props.groupComponent, {}, children); + } +} + +export const VictoryLegend = addEvents(VictoryLegendBase); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2426bbec0..7a059dcf9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -495,10 +495,13 @@ importers: lodash: ^4.17.19 prop-types: ^15.8.1 victory-core: ^36.8.1 + victory-legend: '*' dependencies: lodash: 4.17.21 prop-types: 15.8.1 victory-core: link:../victory-core + devDependencies: + victory-legend: 'link:' packages/victory-line: specifiers: