From 6c00505c5ac6fa902bae30e8443b38a7a58841a9 Mon Sep 17 00:00:00 2001 From: Kenan Date: Fri, 9 Feb 2024 11:09:46 +0000 Subject: [PATCH 01/36] Rewrite victory-container as a function component --- packages/victory-core/src/exports.test.ts | 2 + .../victory-container-fn.tsx | 185 ++++++++++++++++++ .../victory-container/victory-container.tsx | 2 + .../victory-core/src/victory-util/index.ts | 1 + .../src/victory-util/merge-refs.ts | 13 ++ 5 files changed, 203 insertions(+) create mode 100644 packages/victory-core/src/victory-container/victory-container-fn.tsx create mode 100644 packages/victory-core/src/victory-util/merge-refs.ts diff --git a/packages/victory-core/src/exports.test.ts b/packages/victory-core/src/exports.test.ts index df4c844a5..4f926cfa4 100644 --- a/packages/victory-core/src/exports.test.ts +++ b/packages/victory-core/src/exports.test.ts @@ -135,6 +135,7 @@ import { WhiskerProps, Wrapper, addEvents, + mergeRefs, } from "victory-core"; import { pick } from "lodash"; @@ -193,6 +194,7 @@ describe("victory-core", () => { "Whisker", "Wrapper", "addEvents", + "mergeRefs", ] `); }); diff --git a/packages/victory-core/src/victory-container/victory-container-fn.tsx b/packages/victory-core/src/victory-container/victory-container-fn.tsx new file mode 100644 index 000000000..6cfd7f8c6 --- /dev/null +++ b/packages/victory-core/src/victory-container/victory-container-fn.tsx @@ -0,0 +1,185 @@ +import React, { useRef } from "react"; +import { uniqueId } from "lodash"; +import { Portal } from "../victory-portal/portal"; +import { PortalContext } from "../victory-portal/portal-context"; +import TimerContext from "../victory-util/timer-context"; +import * as UserProps from "../victory-util/user-props"; +import { OriginType } from "../victory-label/victory-label"; +import { D3Scale } from "../types/prop-types"; +import { VictoryThemeDefinition } from "../victory-theme/types"; +import { mergeRefs } from "../victory-util"; + +export interface VictoryContainerProps { + "aria-describedby"?: string; + "aria-labelledby"?: string; + children?: React.ReactElement | React.ReactElement[]; + className?: string; + containerId?: number | string; + containerRef?: React.Ref; + desc?: string; + events?: React.DOMAttributes; + height?: number; + name?: string; + origin?: OriginType; + polar?: boolean; + portalComponent?: React.ReactElement; + portalZIndex?: number; + preserveAspectRatio?: string; + responsive?: boolean; + role?: string; + scale?: { + x?: D3Scale; + y?: D3Scale; + }; + style?: React.CSSProperties; + tabIndex?: number; + theme?: VictoryThemeDefinition; + title?: string; + width?: number; + // Props defined by the Open UI Automation (OUIA) 1.0-RC spec + // See https://ouia.readthedocs.io/en/latest/README.html#ouia-component + ouiaId?: number | string; + ouiaSafe?: boolean; + ouiaType?: string; +} + +const defaultProps = { + className: "VictoryContainer", + portalComponent: , + portalZIndex: 99, + responsive: true, + role: "img", +}; + +export const VictoryContainerFn = (initialProps: VictoryContainerProps) => { + const propsWithDefaults = { ...defaultProps, ...initialProps }; + const { + role, + title, + desc, + children, + className, + portalZIndex, + portalComponent, + width, + height, + style, + tabIndex, + responsive, + events, + ouiaId, + ouiaSafe, + ouiaType, + } = propsWithDefaults; + + const containerRef = useRef(null); + + const portalRef = useRef(null); + + // Generated ID stored in ref because it needs to persist across renders + const generatedId = useRef(uniqueId("victory-container-")); + const containerId = propsWithDefaults.containerId ?? generatedId; + + const getIdForElement = (elName: string) => `${containerId}-${elName}`; + + const userProps = UserProps.getSafeUserProps(propsWithDefaults); + + const dimensions = responsive + ? { width: "100%", height: "100%" } + : { width, height }; + + const viewBox = responsive ? `0 0 ${width} ${height}` : undefined; + + const preserveAspectRatio = responsive + ? propsWithDefaults.preserveAspectRatio + : undefined; + + const ariaLabelledBy = + [title && getIdForElement("title"), propsWithDefaults["aria-labelledby"]] + .filter(Boolean) + .join(" ") || undefined; + + const ariaDescribedBy = + [desc && getIdForElement("desc"), propsWithDefaults["aria-describedby"]] + .filter(Boolean) + .join(" ") || undefined; + + const handleWheel = (e: WheelEvent) => e.preventDefault(); + + React.useEffect(() => { + // TODO check that this works + if (!propsWithDefaults.events?.onWheel) return; + + const container = containerRef?.current; + container?.addEventListener("wheel", handleWheel); + + return () => { + container?.removeEventListener("wheel", handleWheel); + }; + }, []); + + return ( + +
+ + {title ? {title} : null} + {desc ? {desc} : null} + {children} + +
+ {React.cloneElement(portalComponent, { + width, + height, + viewBox, + preserveAspectRatio, + style: { ...dimensions, overflow: "visible" }, + ref: portalRef, + })} +
+
+
+ ); +}; + +VictoryContainerFn.role = "container"; +VictoryContainerFn.contextType = TimerContext; diff --git a/packages/victory-core/src/victory-container/victory-container.tsx b/packages/victory-core/src/victory-container/victory-container.tsx index 7020bb44e..ffa5e8011 100644 --- a/packages/victory-core/src/victory-container/victory-container.tsx +++ b/packages/victory-core/src/victory-container/victory-container.tsx @@ -41,6 +41,8 @@ export interface VictoryContainerProps { width?: number; } +export { VictoryContainerFn } from "./victory-container-fn"; + export class VictoryContainer< TProps extends VictoryContainerProps, > extends React.Component { diff --git a/packages/victory-core/src/victory-util/index.ts b/packages/victory-core/src/victory-util/index.ts index 9de3baccb..2788c32ad 100644 --- a/packages/victory-core/src/victory-util/index.ts +++ b/packages/victory-core/src/victory-util/index.ts @@ -1,4 +1,5 @@ export * from "./add-events"; +export * from "./merge-refs"; export * as Axis from "./axis"; export * as Collection from "./collection"; export * from "./common-props"; diff --git a/packages/victory-core/src/victory-util/merge-refs.ts b/packages/victory-core/src/victory-util/merge-refs.ts new file mode 100644 index 000000000..6c5a64067 --- /dev/null +++ b/packages/victory-core/src/victory-util/merge-refs.ts @@ -0,0 +1,13 @@ +type Ref = React.MutableRefObject | React.LegacyRef | undefined | null; + +export function mergeRefs(refs: Ref[]): React.RefCallback { + return (value) => { + refs.forEach((ref) => { + if (typeof ref === "function") { + ref(value); + } else if (ref !== null && ref !== undefined) { + (ref as React.MutableRefObject).current = value; + } + }); + }; +} From b6efcbe411776465c7033b3d3a2dfc965bf79dd2 Mon Sep 17 00:00:00 2001 From: Kenan Date: Mon, 12 Feb 2024 11:41:58 +0000 Subject: [PATCH 02/36] victory-selection-container --- demo/ts/components/selection-demo.tsx | 3 +- .../victory-container-fn.tsx | 2 - .../victory-selection-container/src/index.ts | 1 + .../src/victory-selection-container-fn.tsx | 99 +++++++++++++++++++ 4 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 packages/victory-selection-container/src/victory-selection-container-fn.tsx diff --git a/demo/ts/components/selection-demo.tsx b/demo/ts/components/selection-demo.tsx index 4c986e597..7c4b7da29 100644 --- a/demo/ts/components/selection-demo.tsx +++ b/demo/ts/components/selection-demo.tsx @@ -6,7 +6,8 @@ import { VictoryGroup } from "victory-group"; import { VictoryBar } from "victory-bar"; import { VictoryLine } from "victory-line"; import { VictoryScatter } from "victory-scatter"; -import { VictorySelectionContainer } from "victory-selection-container"; +// import { VictorySelectionContainer } from "victory-selection-container"; +import { VictorySelectionContainerFn as VictorySelectionContainer } from "victory-selection-container"; import { VictoryLegend } from "victory-legend"; import { VictoryTooltip } from "victory-tooltip"; diff --git a/packages/victory-core/src/victory-container/victory-container-fn.tsx b/packages/victory-core/src/victory-container/victory-container-fn.tsx index 6cfd7f8c6..dc7d697a9 100644 --- a/packages/victory-core/src/victory-container/victory-container-fn.tsx +++ b/packages/victory-core/src/victory-container/victory-container-fn.tsx @@ -2,7 +2,6 @@ import React, { useRef } from "react"; import { uniqueId } from "lodash"; import { Portal } from "../victory-portal/portal"; import { PortalContext } from "../victory-portal/portal-context"; -import TimerContext from "../victory-util/timer-context"; import * as UserProps from "../victory-util/user-props"; import { OriginType } from "../victory-label/victory-label"; import { D3Scale } from "../types/prop-types"; @@ -182,4 +181,3 @@ export const VictoryContainerFn = (initialProps: VictoryContainerProps) => { }; VictoryContainerFn.role = "container"; -VictoryContainerFn.contextType = TimerContext; diff --git a/packages/victory-selection-container/src/index.ts b/packages/victory-selection-container/src/index.ts index b779c7988..78ff31cb0 100644 --- a/packages/victory-selection-container/src/index.ts +++ b/packages/victory-selection-container/src/index.ts @@ -1,2 +1,3 @@ export * from "./victory-selection-container"; +export { VictorySelectionContainerFn } from "./victory-selection-container-fn"; export * from "./selection-helpers"; diff --git a/packages/victory-selection-container/src/victory-selection-container-fn.tsx b/packages/victory-selection-container/src/victory-selection-container-fn.tsx new file mode 100644 index 000000000..53a3a74ce --- /dev/null +++ b/packages/victory-selection-container/src/victory-selection-container-fn.tsx @@ -0,0 +1,99 @@ +import React from "react"; +import { + Datum, + Rect, + VictoryContainerFn, + VictoryContainerProps, +} from "victory-core"; +import { SelectionHelpers } from "./selection-helpers"; + +export interface VictorySelectionContainerProps extends VictoryContainerProps { + activateSelectedData?: boolean; + allowSelection?: boolean; + disable?: boolean; + onSelection?: ( + points: { + childName?: string | string[]; + eventKey?: string | number; + data?: Datum[]; + }[], + bounds: { + x: number | Date; + y: number | Date; + }[], + props: VictorySelectionContainerProps, + ) => void; + horizontal?: boolean; + onSelectionCleared?: (props: VictorySelectionContainerProps) => void; + selectionBlacklist?: string[]; + selectionComponent?: React.ReactElement; + selectionDimension?: "x" | "y"; + selectionStyle?: React.CSSProperties; +} + +const defaultProps = { + activateSelectedData: true, + allowSelection: true, + selectionComponent: , + selectionStyle: { + stroke: "transparent", + fill: "black", + fillOpacity: 0.1, + }, +}; + +export const VictorySelectionContainerFn = ( + initialProps: VictorySelectionContainerProps, +) => { + const propsWithDefaults = { ...defaultProps, ...initialProps }; + + const { x1, x2, y1, y2, selectionStyle, selectionComponent, children, name } = + propsWithDefaults; + const width = Math.abs(x2 - x1) || 1; + const height = Math.abs(y2 - y1) || 1; + const x = Math.min(x1, x2); + const y = Math.min(y1, y2); + + const shouldRenderRect = y1 && y2 && x1 && x2; + + return ( + + {children} + + {shouldRenderRect && + React.cloneElement(selectionComponent, { + key: `${name}-selection`, + x, + y, + width, + height, + style: selectionStyle, + })} + + ); +}; + +VictorySelectionContainerFn.role = "container"; + +VictorySelectionContainerFn.defaultEvents = ( + props: VictorySelectionContainerProps, +) => { + const createEventHandler = + (handler: (event: any, targetProps: any) => any) => + (event: any, targetProps: any) => + props.disable ? {} : handler(event, { ...defaultProps, ...targetProps }); + + return [ + { + target: "parent", + eventHandlers: { + onMouseDown: createEventHandler(SelectionHelpers.onMouseDown), + onTouchStart: createEventHandler(SelectionHelpers.onMouseDown), + onMouseMove: createEventHandler(SelectionHelpers.onMouseMove), + onTouchMove: createEventHandler(SelectionHelpers.onMouseMove), + onMouseUp: createEventHandler(SelectionHelpers.onMouseUp), + onTouchEnd: createEventHandler(SelectionHelpers.onMouseUp), + }, + }, + ]; +}; From 7609176c9adea8987e60d4d512c6067095be5486 Mon Sep 17 00:00:00 2001 From: Kenan Date: Mon, 12 Feb 2024 16:23:26 +0000 Subject: [PATCH 03/36] victory-zoom-container --- .../victory-zoom-container-demo.tsx | 19 +- .../victory-container-fn.tsx | 18 +- .../src/victory-selection-container-fn.tsx | 25 +- packages/victory-zoom-container/src/index.ts | 1 + .../src/victory-zoom-container-fn.tsx | 219 ++++++++++++++++++ 5 files changed, 261 insertions(+), 21 deletions(-) create mode 100644 packages/victory-zoom-container/src/victory-zoom-container-fn.tsx diff --git a/demo/ts/components/victory-zoom-container-demo.tsx b/demo/ts/components/victory-zoom-container-demo.tsx index 2eab1a3af..bd9b147c9 100644 --- a/demo/ts/components/victory-zoom-container-demo.tsx +++ b/demo/ts/components/victory-zoom-container-demo.tsx @@ -9,7 +9,8 @@ import { VictoryArea } from "victory-area"; import { VictoryBar } from "victory-bar"; import { VictoryLine } from "victory-line"; import { VictoryScatter } from "victory-scatter"; -import { VictoryZoomContainer } from "victory-zoom-container"; +import { VictoryZoomContainerFn as VictoryZoomContainer } from "victory-zoom-container"; +// import { VictoryZoomContainer } from "victory-zoom-container"; import { VictoryTooltip } from "victory-tooltip"; import { VictoryLegend } from "victory-legend"; import { @@ -331,7 +332,9 @@ export default class VictoryZoomContainerDemo extends React.Component< { mutation: (props) => { return { - style: Object.assign({}, props.style, { stroke: "orange" }), + style: Object.assign({}, props.style, { + stroke: "orange", + }), }; }, }, @@ -417,7 +420,9 @@ export default class VictoryZoomContainerDemo extends React.Component< target: "data", mutation: (props) => { return { - style: Object.assign({}, props.style, { fill: "gold" }), + style: Object.assign({}, props.style, { + fill: "gold", + }), }; }, }, @@ -426,7 +431,9 @@ export default class VictoryZoomContainerDemo extends React.Component< target: "data", mutation: (props) => { return { - style: Object.assign({}, props.style, { fill: "orange" }), + style: Object.assign({}, props.style, { + fill: "orange", + }), }; }, }, @@ -435,7 +442,9 @@ export default class VictoryZoomContainerDemo extends React.Component< target: "data", mutation: (props) => { return { - style: Object.assign({}, props.style, { fill: "red" }), + style: Object.assign({}, props.style, { + fill: "red", + }), }; }, }, diff --git a/packages/victory-core/src/victory-container/victory-container-fn.tsx b/packages/victory-core/src/victory-container/victory-container-fn.tsx index dc7d697a9..dbeb500a0 100644 --- a/packages/victory-core/src/victory-container/victory-container-fn.tsx +++ b/packages/victory-core/src/victory-container/victory-container-fn.tsx @@ -51,7 +51,7 @@ const defaultProps = { }; export const VictoryContainerFn = (initialProps: VictoryContainerProps) => { - const propsWithDefaults = { ...defaultProps, ...initialProps }; + const props = { ...defaultProps, ...initialProps }; const { role, title, @@ -69,7 +69,7 @@ export const VictoryContainerFn = (initialProps: VictoryContainerProps) => { ouiaId, ouiaSafe, ouiaType, - } = propsWithDefaults; + } = props; const containerRef = useRef(null); @@ -77,11 +77,11 @@ export const VictoryContainerFn = (initialProps: VictoryContainerProps) => { // Generated ID stored in ref because it needs to persist across renders const generatedId = useRef(uniqueId("victory-container-")); - const containerId = propsWithDefaults.containerId ?? generatedId; + const containerId = props.containerId ?? generatedId; const getIdForElement = (elName: string) => `${containerId}-${elName}`; - const userProps = UserProps.getSafeUserProps(propsWithDefaults); + const userProps = UserProps.getSafeUserProps(props); const dimensions = responsive ? { width: "100%", height: "100%" } @@ -90,16 +90,16 @@ export const VictoryContainerFn = (initialProps: VictoryContainerProps) => { const viewBox = responsive ? `0 0 ${width} ${height}` : undefined; const preserveAspectRatio = responsive - ? propsWithDefaults.preserveAspectRatio + ? props.preserveAspectRatio : undefined; const ariaLabelledBy = - [title && getIdForElement("title"), propsWithDefaults["aria-labelledby"]] + [title && getIdForElement("title"), props["aria-labelledby"]] .filter(Boolean) .join(" ") || undefined; const ariaDescribedBy = - [desc && getIdForElement("desc"), propsWithDefaults["aria-describedby"]] + [desc && getIdForElement("desc"), props["aria-describedby"]] .filter(Boolean) .join(" ") || undefined; @@ -107,7 +107,7 @@ export const VictoryContainerFn = (initialProps: VictoryContainerProps) => { React.useEffect(() => { // TODO check that this works - if (!propsWithDefaults.events?.onWheel) return; + if (!props.events?.onWheel) return; const container = containerRef?.current; container?.addEventListener("wheel", handleWheel); @@ -138,7 +138,7 @@ export const VictoryContainerFn = (initialProps: VictoryContainerProps) => { data-ouia-component-id={ouiaId} data-ouia-component-type={ouiaType} data-ouia-safe={ouiaSafe} - ref={mergeRefs([containerRef, propsWithDefaults.containerRef])} + ref={mergeRefs([containerRef, props.containerRef])} > void; + const defaultProps = { activateSelectedData: true, allowSelection: true, @@ -45,10 +52,10 @@ const defaultProps = { export const VictorySelectionContainerFn = ( initialProps: VictorySelectionContainerProps, ) => { - const propsWithDefaults = { ...defaultProps, ...initialProps }; + const props = { ...defaultProps, ...initialProps }; const { x1, x2, y1, y2, selectionStyle, selectionComponent, children, name } = - propsWithDefaults; + props; const width = Math.abs(x2 - x1) || 1; const height = Math.abs(y2 - y1) || 1; const x = Math.min(x1, x2); @@ -57,7 +64,7 @@ export const VictorySelectionContainerFn = ( const shouldRenderRect = y1 && y2 && x1 && x2; return ( - + {children} {shouldRenderRect && @@ -76,12 +83,16 @@ export const VictorySelectionContainerFn = ( VictorySelectionContainerFn.role = "container"; VictorySelectionContainerFn.defaultEvents = ( - props: VictorySelectionContainerProps, + initialProps: VictorySelectionContainerProps, ) => { + const props = { ...defaultProps, ...initialProps }; const createEventHandler = - (handler: (event: any, targetProps: any) => any) => - (event: any, targetProps: any) => - props.disable ? {} : handler(event, { ...defaultProps, ...targetProps }); + (handler: Handler, disabled?: boolean): Handler => + // eslint-disable-next-line max-params + (event, targetProps, eventKey, context) => + disabled || props.disable + ? {} + : handler(event, { ...props, ...targetProps }, eventKey, context); return [ { diff --git a/packages/victory-zoom-container/src/index.ts b/packages/victory-zoom-container/src/index.ts index 1a87c305d..05098fdfd 100644 --- a/packages/victory-zoom-container/src/index.ts +++ b/packages/victory-zoom-container/src/index.ts @@ -1,2 +1,3 @@ export * from "./victory-zoom-container"; +export { VictoryZoomContainerFn } from "./victory-zoom-container-fn"; export * from "./zoom-helpers"; diff --git a/packages/victory-zoom-container/src/victory-zoom-container-fn.tsx b/packages/victory-zoom-container/src/victory-zoom-container-fn.tsx new file mode 100644 index 000000000..13672a3c8 --- /dev/null +++ b/packages/victory-zoom-container/src/victory-zoom-container-fn.tsx @@ -0,0 +1,219 @@ +import React from "react"; +import { ZoomHelpers } from "./zoom-helpers"; +import { + VictoryClipContainer, + Data, + VictoryContainerProps, + DomainTuple, + VictoryContainerFn, +} from "victory-core"; +import { defaults } from "lodash"; + +const DEFAULT_DOWNSAMPLE = 150; + +export type ZoomDimensionType = "x" | "y"; + +type ZoomDomain = { + x: DomainTuple; + y: DomainTuple; +}; + +export interface VictoryZoomContainerProps extends VictoryContainerProps { + allowPan?: boolean; + allowZoom?: boolean; + clipContainerComponent?: React.ReactElement; + disable?: boolean; + downsample?: number | boolean; + minimumZoom?: { x?: number; y?: number }; + onZoomDomainChange?: ( + domain: ZoomDomain, + props: VictoryZoomContainerProps, + ) => void; + zoomDimension?: ZoomDimensionType; + zoomDomain?: Partial; +} + +type Handler = ( + event: any, + targetProps: any, + eventKey?: any, + context?: any, +) => void; + +const defaultProps = { + clipContainerComponent: , + allowPan: true, + allowZoom: true, + zoomActive: false, +}; + +export const VictoryZoomContainerFn = ( + initialProps: VictoryZoomContainerProps, +) => { + const props = { ...defaultProps, ...initialProps }; + const { + children, + currentDomain, + zoomActive, + allowZoom, + downsample, + scale, + clipContainerComponent, + polar, + origin, + horizontal, + } = props; + + const downsampleZoomData = (child: React.ReactElement, domain) => { + const getData = (childProps) => { + const { data, x, y } = childProps; + const defaultGetData = + child.type && typeof (child.type as any).getData === "function" + ? (child.type as any).getData + : () => undefined; + // skip costly data formatting if x and y accessors are not present + return Array.isArray(data) && !x && !y + ? data + : defaultGetData(childProps); + }; + + const data = getData(child.props); + + // return undefined if downsample is not run, then default() will replace with child.props.data + if (!downsample || !domain || !data) { + return undefined; + } + + const maxPoints = downsample === true ? DEFAULT_DOWNSAMPLE : downsample; + const dimension = props.zoomDimension || "x"; + + // important: assumes data is ordered by dimension + // get the start and end of the data that is in the current visible domain + let startIndex = data.findIndex( + (d) => d[dimension] >= domain[dimension][0], + ); + let endIndex = data.findIndex((d) => d[dimension] > domain[dimension][1]); + // pick one more point (if available) at each end so that VictoryLine, VictoryArea connect + if (startIndex !== 0) { + startIndex -= 1; + } + if (endIndex !== -1) { + endIndex += 1; + } + + const visibleData = data.slice(startIndex, endIndex); + + return Data.downsample(visibleData, maxPoints, startIndex); + }; + + const modifiedChildren = ( + React.Children.toArray(children) as React.ReactElement[] + ).map((child) => { + const role = (child as any).type && (child as any).type.role; + const isDataComponent = Data.isDataComponent(child); + const originalDomain = defaults({}, props.originalDomain, props.domain); + const zoomDomain = defaults({}, props.zoomDomain, props.domain); + const cachedZoomDomain = defaults({}, props.cachedZoomDomain, props.domain); + + let domain: ZoomDomain; + + if (!ZoomHelpers.checkDomainEquality(zoomDomain, cachedZoomDomain)) { + // if zoomDomain has been changed, use it + domain = zoomDomain; + } else if (allowZoom && !zoomActive) { + // if user has zoomed all the way out, use the child domain + domain = child.props.domain; + } else { + // default: use currentDomain, set by the event handlers + domain = defaults({}, currentDomain, originalDomain); + } + + let newDomain = props.polar + ? { + x: originalDomain.x, + y: [0, domain.y[1]], + } + : domain; + + if (newDomain && props.zoomDimension) { + // if zooming is restricted to a dimension, don't squash changes to zoomDomain in other dim + newDomain = { + ...zoomDomain, + [props.zoomDimension]: newDomain[props.zoomDimension], + }; + } + + // don't downsample stacked data + const childProps = + isDataComponent && role !== "stack" + ? { + domain: newDomain, + data: downsampleZoomData(child, newDomain), + } + : { domain: newDomain }; + + const newChild = React.cloneElement( + child, + defaults(childProps, child.props), + ); + + // Clip data components + if (Data.isDataComponent(newChild)) { + const rangeX = horizontal ? scale.y.range() : scale.x.range(); + const rangeY = horizontal ? scale.x.range() : scale.y.range(); + const plottableWidth = Math.abs(rangeX[0] - rangeX[1]); + const plottableHeight = Math.abs(rangeY[0] - rangeY[1]); + const radius = Math.max(...rangeY); + const groupComponent = React.cloneElement(clipContainerComponent, { + clipWidth: plottableWidth, + clipHeight: plottableHeight, + translateX: Math.min(...rangeX), + translateY: Math.min(...rangeY), + polar, + origin: polar ? origin : undefined, + radius: polar ? radius : undefined, + ...clipContainerComponent.props, + }); + + return React.cloneElement(newChild, { + groupComponent, + }); + } + + return newChild; + }); + + return {modifiedChildren}; +}; + +VictoryZoomContainerFn.role = "container"; + +VictoryZoomContainerFn.defaultEvents = ( + initialProps: VictoryZoomContainerProps, +) => { + const props = { ...defaultProps, ...initialProps }; + const createEventHandler = + (handler: Handler, disabled?: boolean): Handler => + // eslint-disable-next-line max-params + (event, targetProps, eventKey, context) => + disabled || props.disable + ? {} + : handler(event, { ...props, ...targetProps }, eventKey, context); + + return [ + { + target: "parent", + eventHandlers: { + onMouseDown: createEventHandler(ZoomHelpers.onMouseDown), + onTouchStart: createEventHandler(ZoomHelpers.onMouseDown), + onMouseUp: createEventHandler(ZoomHelpers.onMouseUp), + onTouchEnd: createEventHandler(ZoomHelpers.onMouseUp), + onMouseLeave: createEventHandler(ZoomHelpers.onMouseLeave), + onTouchCancel: createEventHandler(ZoomHelpers.onMouseLeave), + onMouseMove: createEventHandler(ZoomHelpers.onMouseMove), + onTouchMove: createEventHandler(ZoomHelpers.onMouseMove), + onWheel: createEventHandler(ZoomHelpers.onWheel, !props.allowZoom), + }, + }, + ]; +}; From 86f2ee4ccbfcf02fb8fc5e7534988926c203c0b4 Mon Sep 17 00:00:00 2001 From: Kenan Date: Tue, 13 Feb 2024 11:24:36 +0000 Subject: [PATCH 04/36] working create-container function --- demo/ts/components/create-container-demo.tsx | 8 +- .../src/create-container-fn.tsx | 128 ++++++++++++++++++ .../victory-create-container/src/index.ts | 1 + .../victory-selection-container/src/index.ts | 5 +- .../src/victory-selection-container-fn.tsx | 39 +++--- packages/victory-zoom-container/src/index.ts | 5 +- .../src/victory-zoom-container-fn.tsx | 29 ++-- 7 files changed, 182 insertions(+), 33 deletions(-) create mode 100644 packages/victory-create-container/src/create-container-fn.tsx diff --git a/demo/ts/components/create-container-demo.tsx b/demo/ts/components/create-container-demo.tsx index 778504cf0..908c7634c 100644 --- a/demo/ts/components/create-container-demo.tsx +++ b/demo/ts/components/create-container-demo.tsx @@ -5,7 +5,7 @@ import { round } from "lodash"; import { VictoryChart } from "victory-chart"; import { VictoryStack } from "victory-stack"; import { VictoryGroup } from "victory-group"; -import { createContainer } from "victory-create-container"; +import { createContainerFn as createContainer } from "victory-create-container"; import { VictoryBar } from "victory-bar"; import { VictoryLine } from "victory-line"; import { VictoryScatter } from "victory-scatter"; @@ -273,10 +273,10 @@ class App extends React.Component { render() { return (
- - + + {/* - + */}
); } diff --git a/packages/victory-create-container/src/create-container-fn.tsx b/packages/victory-create-container/src/create-container-fn.tsx new file mode 100644 index 000000000..8dcd39395 --- /dev/null +++ b/packages/victory-create-container/src/create-container-fn.tsx @@ -0,0 +1,128 @@ +import { + VictoryZoomContainerFn, + useVictoryZoomContainer, +} from "victory-zoom-container"; +import { + VictorySelectionContainerFn, + useVictorySelectionContainer, +} from "victory-selection-container"; +import React from "react"; +import { VictoryContainerFn } from "victory-core"; +import { forOwn, groupBy, isEmpty, toPairs } from "lodash"; + +export type ContainerType = + | "brush" + | "cursor" + | "selection" + | "voronoi" + | "zoom"; + +const CONTAINERS: { + [key in ContainerType]: { + name: string; + component: React.ComponentType; + hook: (props: any) => { + props: any; + children: React.ReactNode; + }; + }; +} = { + zoom: { + name: "Zoom", + component: VictoryZoomContainerFn, + hook: useVictoryZoomContainer, + }, + selection: { + name: "Selection", + component: VictorySelectionContainerFn, + hook: useVictorySelectionContainer, + }, +}; + +function ensureArray(thing: T): [] | T | T[] { + if (!thing) { + return []; + } else if (!Array.isArray(thing)) { + return [thing]; + } + return thing; +} + +const combineEventHandlers = (eventHandlersArray: any[]) => { + // takes an array of event handler objects and produces one eventHandlers object + // creates a custom combinedHandler() for events with multiple conflicting handlers + return eventHandlersArray.reduce((localHandlers, finalHandlers) => { + forOwn(localHandlers, (localHandler, eventName) => { + const existingHandler = finalHandlers[eventName]; + if (existingHandler) { + // create new handler for event that concats the existing handler's mutations with new ones + finalHandlers[eventName] = function combinedHandler(...params) { + // named for debug clarity + // sometimes handlers return undefined; use empty array instead, for concat() + const existingMutations = ensureArray(existingHandler(...params)); + const localMutations = ensureArray(localHandler(...params)); + return existingMutations.concat(localMutations); + }; + } else { + finalHandlers[eventName] = localHandler; + } + }); + return finalHandlers; + }); +}; + +const combineDefaultEvents = (defaultEvents: any[]) => { + // takes a defaultEvents array and returns one equal or lesser length, + // by combining any events that have the same target + const eventsByTarget = groupBy(defaultEvents, "target"); + const events = toPairs(eventsByTarget).map(([target, eventsArray]) => { + const newEventsArray = eventsArray.filter(Boolean); + return isEmpty(newEventsArray) + ? null + : { + target, + eventHandlers: combineEventHandlers( + eventsArray.map((event) => event.eventHandlers), + ), + // note: does not currently handle eventKey or childName + }; + }); + return events.filter(Boolean); +}; + +// TODO: Type this function properly +export function createContainerFn( + containerA: ContainerType, + containerB: ContainerType, +) { + const { + name: containerAName, + component: ContainerA, + hook: useContainerA, + } = CONTAINERS[containerA]; + const { + name: containerBName, + component: ContainerB, + hook: useContainerB, + } = CONTAINERS[containerB]; + + function NewContainer(props: any) { + const { children: childrenA, props: propsA } = useContainerA(props); + const { children: childrenB, props: propsB } = useContainerB({ + ...propsA, + children: childrenA, + }); + + return {childrenB}; + } + + NewContainer.displayName = `Victory${containerAName}${containerBName}Container`; + NewContainer.role = "container"; + NewContainer.defaultEvents = (props: any) => + combineDefaultEvents([ + ...ContainerA.defaultEvents(props), + ...ContainerB.defaultEvents(props), + ]); + + return NewContainer; +} diff --git a/packages/victory-create-container/src/index.ts b/packages/victory-create-container/src/index.ts index 5f9b97723..a7c0a1c7b 100644 --- a/packages/victory-create-container/src/index.ts +++ b/packages/victory-create-container/src/index.ts @@ -1 +1,2 @@ export * from "./create-container"; +export { createContainerFn } from "./create-container-fn"; diff --git a/packages/victory-selection-container/src/index.ts b/packages/victory-selection-container/src/index.ts index 78ff31cb0..a40972ae1 100644 --- a/packages/victory-selection-container/src/index.ts +++ b/packages/victory-selection-container/src/index.ts @@ -1,3 +1,6 @@ export * from "./victory-selection-container"; -export { VictorySelectionContainerFn } from "./victory-selection-container-fn"; +export { + VictorySelectionContainerFn, + useVictorySelectionContainer, +} from "./victory-selection-container-fn"; export * from "./selection-helpers"; diff --git a/packages/victory-selection-container/src/victory-selection-container-fn.tsx b/packages/victory-selection-container/src/victory-selection-container-fn.tsx index 6969b6b1d..b2698b776 100644 --- a/packages/victory-selection-container/src/victory-selection-container-fn.tsx +++ b/packages/victory-selection-container/src/victory-selection-container-fn.tsx @@ -7,6 +7,13 @@ import { } from "victory-core"; import { SelectionHelpers } from "./selection-helpers"; +type Handler = ( + event: any, + targetProps: any, + eventKey?: any, + context?: any, +) => void; + export interface VictorySelectionContainerProps extends VictoryContainerProps { activateSelectedData?: boolean; allowSelection?: boolean; @@ -31,13 +38,6 @@ export interface VictorySelectionContainerProps extends VictoryContainerProps { selectionStyle?: React.CSSProperties; } -type Handler = ( - event: any, - targetProps: any, - eventKey?: any, - context?: any, -) => void; - const defaultProps = { activateSelectedData: true, allowSelection: true, @@ -49,7 +49,7 @@ const defaultProps = { }, }; -export const VictorySelectionContainerFn = ( +export const useVictorySelectionContainer = ( initialProps: VictorySelectionContainerProps, ) => { const props = { ...defaultProps, ...initialProps }; @@ -63,11 +63,11 @@ export const VictorySelectionContainerFn = ( const shouldRenderRect = y1 && y2 && x1 && x2; - return ( - - {children} - - {shouldRenderRect && + return { + props, + children: [ + children, + shouldRenderRect && React.cloneElement(selectionComponent, { key: `${name}-selection`, x, @@ -75,9 +75,16 @@ export const VictorySelectionContainerFn = ( width, height, style: selectionStyle, - })} - - ); + }), + ], + }; +}; + +export const VictorySelectionContainerFn = ( + initialProps: VictorySelectionContainerProps, +) => { + const { props, children } = useVictorySelectionContainer(initialProps); + return {children}; }; VictorySelectionContainerFn.role = "container"; diff --git a/packages/victory-zoom-container/src/index.ts b/packages/victory-zoom-container/src/index.ts index 05098fdfd..eba9c925f 100644 --- a/packages/victory-zoom-container/src/index.ts +++ b/packages/victory-zoom-container/src/index.ts @@ -1,3 +1,6 @@ export * from "./victory-zoom-container"; -export { VictoryZoomContainerFn } from "./victory-zoom-container-fn"; +export { + VictoryZoomContainerFn, + useVictoryZoomContainer, +} from "./victory-zoom-container-fn"; export * from "./zoom-helpers"; diff --git a/packages/victory-zoom-container/src/victory-zoom-container-fn.tsx b/packages/victory-zoom-container/src/victory-zoom-container-fn.tsx index 13672a3c8..87a7e7326 100644 --- a/packages/victory-zoom-container/src/victory-zoom-container-fn.tsx +++ b/packages/victory-zoom-container/src/victory-zoom-container-fn.tsx @@ -2,10 +2,10 @@ import React from "react"; import { ZoomHelpers } from "./zoom-helpers"; import { VictoryClipContainer, - Data, VictoryContainerProps, DomainTuple, VictoryContainerFn, + Data, } from "victory-core"; import { defaults } from "lodash"; @@ -13,11 +13,18 @@ const DEFAULT_DOWNSAMPLE = 150; export type ZoomDimensionType = "x" | "y"; -type ZoomDomain = { +export type ZoomDomain = { x: DomainTuple; y: DomainTuple; }; +type Handler = ( + event: any, + targetProps: any, + eventKey?: any, + context?: any, +) => void; + export interface VictoryZoomContainerProps extends VictoryContainerProps { allowPan?: boolean; allowZoom?: boolean; @@ -33,13 +40,6 @@ export interface VictoryZoomContainerProps extends VictoryContainerProps { zoomDomain?: Partial; } -type Handler = ( - event: any, - targetProps: any, - eventKey?: any, - context?: any, -) => void; - const defaultProps = { clipContainerComponent: , allowPan: true, @@ -47,7 +47,7 @@ const defaultProps = { zoomActive: false, }; -export const VictoryZoomContainerFn = ( +export const useVictoryZoomContainer = ( initialProps: VictoryZoomContainerProps, ) => { const props = { ...defaultProps, ...initialProps }; @@ -183,7 +183,14 @@ export const VictoryZoomContainerFn = ( return newChild; }); - return {modifiedChildren}; + return { props, children: modifiedChildren }; +}; + +export const VictoryZoomContainerFn = ( + initialProps: VictoryZoomContainerProps, +) => { + const { props, children } = useVictoryZoomContainer(initialProps); + return {children}; }; VictoryZoomContainerFn.role = "container"; From 15e31eb3701d6dcc9237cba78b72cfb9bfbba446 Mon Sep 17 00:00:00 2001 From: Kenan Date: Tue, 13 Feb 2024 11:37:45 +0000 Subject: [PATCH 05/36] wip --- .../src/create-container-fn.tsx | 59 ++++++++++++------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/packages/victory-create-container/src/create-container-fn.tsx b/packages/victory-create-container/src/create-container-fn.tsx index 8dcd39395..2c4c2656f 100644 --- a/packages/victory-create-container/src/create-container-fn.tsx +++ b/packages/victory-create-container/src/create-container-fn.tsx @@ -17,28 +17,6 @@ export type ContainerType = | "voronoi" | "zoom"; -const CONTAINERS: { - [key in ContainerType]: { - name: string; - component: React.ComponentType; - hook: (props: any) => { - props: any; - children: React.ReactNode; - }; - }; -} = { - zoom: { - name: "Zoom", - component: VictoryZoomContainerFn, - hook: useVictoryZoomContainer, - }, - selection: { - name: "Selection", - component: VictorySelectionContainerFn, - hook: useVictorySelectionContainer, - }, -}; - function ensureArray(thing: T): [] | T | T[] { if (!thing) { return []; @@ -90,6 +68,43 @@ const combineDefaultEvents = (defaultEvents: any[]) => { return events.filter(Boolean); }; +type Container = { + name: string; + component: React.FC; + hook: (props: any) => { + props: any; + children: React.ReactNode; + }; +}; + +const CONTAINERS: Record = { + zoom: { + name: "Zoom", + component: VictoryZoomContainerFn, + hook: useVictoryZoomContainer, + }, + selection: { + name: "Selection", + component: VictorySelectionContainerFn, + hook: useVictorySelectionContainer, + }, + brush: { + name: "Brush", + component: VictoryZoomContainerFn, + hook: useVictoryZoomContainer, + }, + cursor: { + name: "Cursor", + component: VictoryZoomContainerFn, + hook: useVictoryZoomContainer, + }, + voronoi: { + name: "Voronoi", + component: VictoryZoomContainerFn, + hook: useVictoryZoomContainer, + }, +}; + // TODO: Type this function properly export function createContainerFn( containerA: ContainerType, From 9ed6257f4aa2fd9643e162539c013cad5ca55498 Mon Sep 17 00:00:00 2001 From: Kenan Date: Tue, 13 Feb 2024 14:29:12 +0000 Subject: [PATCH 06/36] victory-voronoi-container --- .../victory-voronoi-container-demo.tsx | 3 +- .../src/create-container-fn.tsx | 9 +- .../victory-voronoi-container/src/index.ts | 4 + .../src/victory-voronoi-container-fn.tsx | 242 ++++++++++++++++++ 4 files changed, 255 insertions(+), 3 deletions(-) create mode 100644 packages/victory-voronoi-container/src/victory-voronoi-container-fn.tsx diff --git a/demo/ts/components/victory-voronoi-container-demo.tsx b/demo/ts/components/victory-voronoi-container-demo.tsx index eee4f9499..22ccdf6c9 100644 --- a/demo/ts/components/victory-voronoi-container-demo.tsx +++ b/demo/ts/components/victory-voronoi-container-demo.tsx @@ -7,7 +7,8 @@ import { VictoryGroup } from "victory-group"; import { VictoryBar } from "victory-bar"; import { VictoryLine } from "victory-line"; import { VictoryScatter } from "victory-scatter"; -import { VictoryVoronoiContainer } from "victory-voronoi-container"; +// import { VictoryVoronoiContainer } from "victory-voronoi-container"; +import { VictoryVoronoiContainerFn as VictoryVoronoiContainer } from "victory-voronoi-container"; import { Flyout, VictoryTooltip } from "victory-tooltip"; import { VictoryLegend } from "victory-legend"; import { VictoryLabel, VictoryTheme } from "victory-core"; diff --git a/packages/victory-create-container/src/create-container-fn.tsx b/packages/victory-create-container/src/create-container-fn.tsx index 2c4c2656f..e9b4a50d5 100644 --- a/packages/victory-create-container/src/create-container-fn.tsx +++ b/packages/victory-create-container/src/create-container-fn.tsx @@ -9,6 +9,10 @@ import { import React from "react"; import { VictoryContainerFn } from "victory-core"; import { forOwn, groupBy, isEmpty, toPairs } from "lodash"; +import { + VictoryVoronoiContainerFn, + useVictoryVoronoiContainer, +} from "victory-voronoi-container"; export type ContainerType = | "brush" @@ -77,6 +81,7 @@ type Container = { }; }; +// TODO: Add correct components & hooks when they are built const CONTAINERS: Record = { zoom: { name: "Zoom", @@ -100,8 +105,8 @@ const CONTAINERS: Record = { }, voronoi: { name: "Voronoi", - component: VictoryZoomContainerFn, - hook: useVictoryZoomContainer, + component: VictoryVoronoiContainerFn, + hook: useVictoryVoronoiContainer, }, }; diff --git a/packages/victory-voronoi-container/src/index.ts b/packages/victory-voronoi-container/src/index.ts index 313e652a9..48d73f9d0 100644 --- a/packages/victory-voronoi-container/src/index.ts +++ b/packages/victory-voronoi-container/src/index.ts @@ -1,2 +1,6 @@ export * from "./victory-voronoi-container"; +export { + VictoryVoronoiContainerFn, + useVictoryVoronoiContainer, +} from "./victory-voronoi-container-fn"; export * from "./voronoi-helpers"; diff --git a/packages/victory-voronoi-container/src/victory-voronoi-container-fn.tsx b/packages/victory-voronoi-container/src/victory-voronoi-container-fn.tsx new file mode 100644 index 000000000..8690f68d5 --- /dev/null +++ b/packages/victory-voronoi-container/src/victory-voronoi-container-fn.tsx @@ -0,0 +1,242 @@ +/* eslint-disable react/no-multi-comp */ +import React from "react"; +import { defaults, pick } from "lodash"; +import { VictoryTooltip } from "victory-tooltip"; +import { + Helpers, + VictoryContainerProps, + PaddingProps, + VictoryContainerFn, +} from "victory-core"; +import { VoronoiHelpers } from "./voronoi-helpers"; + +type Handler = ( + event: any, + targetProps: any, + eventKey?: any, + context?: any, +) => void; + +export interface VictoryVoronoiContainerProps extends VictoryContainerProps { + activateData?: boolean; + activateLabels?: boolean; + disable?: boolean; + labels?: (point: any, index?: number, points?: any[]) => string; + labelComponent?: React.ReactElement; + mouseFollowTooltips?: boolean; + onActivated?: (points: any[], props: VictoryVoronoiContainerProps) => void; + onDeactivated?: (points: any[], props: VictoryVoronoiContainerProps) => void; + radius?: number; + voronoiBlacklist?: (string | RegExp)[]; + voronoiDimension?: "x" | "y"; + voronoiPadding?: PaddingProps; +} + +const defaultProps = { + activateData: true, + activateLabels: true, + labelComponent: , + voronoiPadding: 5, +}; + +const getPoint = (point) => { + const whitelist = ["_x", "_x1", "_x0", "_y", "_y1", "_y0"]; + return pick(point, whitelist); +}; + +export const useVictoryVoronoiContainer = ( + initialProps: VictoryVoronoiContainerProps, +) => { + const props = { ...defaultProps, ...initialProps }; + const { children } = props; + + const getDimension = () => { + const { horizontal, voronoiDimension } = props; + if (!horizontal || !voronoiDimension) { + return voronoiDimension; + } + return voronoiDimension === "x" ? "y" : "x"; + }; + + const getLabelPosition = (labelProps, points) => { + const { mousePosition, mouseFollowTooltips } = props; + const voronoiDimension = getDimension(); + const point = getPoint(points[0]); + const basePosition = Helpers.scalePoint(props, point); + + let center = mouseFollowTooltips ? mousePosition : undefined; + if (!voronoiDimension || points.length < 2) { + return { + ...basePosition, + center: defaults({}, labelProps.center, center), + }; + } + + const x = voronoiDimension === "y" ? mousePosition.x : basePosition.x; + const y = voronoiDimension === "x" ? mousePosition.y : basePosition.y; + center = mouseFollowTooltips ? mousePosition : { x, y }; + return { x, y, center: defaults({}, labelProps.center, center) }; + }; + + const getStyle = (points, type) => { + const { labels, labelComponent, theme } = props; + const componentProps = labelComponent.props || {}; + const themeStyles = + theme && theme.voronoi && theme.voronoi.style ? theme.voronoi.style : {}; + const componentStyleArray = + type === "flyout" ? componentProps.flyoutStyle : componentProps.style; + return points.reduce((memo, datum, index) => { + const labelProps = defaults({}, componentProps, { + datum, + active: true, + }); + const text = Helpers.isFunction(labels) ? labels(labelProps) : undefined; + const textArray = text !== undefined ? `${text}`.split("\n") : []; + const baseStyle = (datum.style && datum.style[type]) || {}; + const componentStyle = Array.isArray(componentStyleArray) + ? componentStyleArray[index] + : componentStyleArray; + const style = Helpers.evaluateStyle( + defaults({}, componentStyle, baseStyle, themeStyles[type]), + labelProps, + ); + const styleArray = textArray.length + ? textArray.map(() => style) + : [style]; + return memo.concat(styleArray); + }, []); + }; + + const getDefaultLabelProps = (points) => { + const { voronoiDimension, horizontal, mouseFollowTooltips } = props; + const point = getPoint(points[0]); + const multiPoint = voronoiDimension && points.length > 1; + const y = point._y1 !== undefined ? point._y1 : point._y; + const defaultHorizontalOrientation = y < 0 ? "left" : "right"; + const defaultOrientation = y < 0 ? "bottom" : "top"; + const labelOrientation = horizontal + ? defaultHorizontalOrientation + : defaultOrientation; + const orientation = mouseFollowTooltips ? undefined : labelOrientation; + return { + orientation, + pointerLength: multiPoint ? 0 : undefined, + constrainToVisibleArea: + multiPoint || mouseFollowTooltips ? true : undefined, + }; + }; + + const getLabelProps = (points) => { + const { labels, scale, labelComponent, theme, width, height } = props; + const componentProps = labelComponent.props || {}; + const text = points.reduce((memo, datum) => { + const labelProps = defaults({}, componentProps, { + datum, + active: true, + }); + const t = Helpers.isFunction(labels) ? labels(labelProps) : null; + if (t === null || t === undefined) { + return memo; + } + return memo.concat(`${t}`.split("\n")); + }, []); + + // remove properties from first point to make datum + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { childName, eventKey, style, continuous, ...datum } = points[0]; + const name = + props.name === childName ? childName : `${props.name}-${childName}`; + const labelProps = defaults( + { + key: `${name}-${eventKey}-voronoi-tooltip`, + id: `${name}-${eventKey}-voronoi-tooltip`, + active: true, + renderInPortal: false, + activePoints: points, + datum, + scale, + theme, + }, + componentProps, + { + text, + width, + height, + style: getStyle(points, "labels"), + flyoutStyle: getStyle(points, "flyout")[0], + }, + getDefaultLabelProps(points), + ); + const labelPosition = getLabelPosition(labelProps, points); + + return defaults({}, labelPosition, labelProps); + }; + + const getTooltip = () => { + const { labels, activePoints, labelComponent } = props; + if (!labels) { + return null; + } + if (Array.isArray(activePoints) && activePoints.length) { + const labelProps = getLabelProps(activePoints); + const { text } = labelProps; + const showLabel = Array.isArray(text) + ? text.filter(Boolean).length + : text; + return showLabel ? React.cloneElement(labelComponent, labelProps) : null; + } + return null; + }; + + return { + props, + children: [ + ...React.Children.toArray(children), + getTooltip(), + ] as React.ReactElement[], + }; +}; + +export const VictoryVoronoiContainerFn = ( + initialProps: VictoryVoronoiContainerProps, +) => { + const { props, children } = useVictoryVoronoiContainer(initialProps); + return {children}; +}; + +VictoryVoronoiContainerFn.role = "container"; + +VictoryVoronoiContainerFn.defaultEvents = ( + initialProps: VictoryVoronoiContainerProps, +) => { + const props = { ...defaultProps, ...initialProps }; + const createEventHandler = + (handler: Handler, disabled?: boolean): Handler => + // eslint-disable-next-line max-params + (event, targetProps, eventKey, context) => + disabled || props.disable + ? {} + : handler(event, { ...props, ...targetProps }, eventKey, context); + + return [ + { + target: "parent", + eventHandlers: { + onMouseLeave: createEventHandler(VoronoiHelpers.onMouseLeave), + onTouchCancel: createEventHandler(VoronoiHelpers.onMouseLeave), + onMouseMove: createEventHandler(VoronoiHelpers.onMouseMove), + onTouchMove: createEventHandler(VoronoiHelpers.onMouseMove), + }, + }, + { + target: "data", + eventHandlers: props.disable + ? {} + : { + onMouseOver: () => null, + onMouseOut: () => null, + onMouseMove: () => null, + }, + }, + ]; +}; From d08957374400c315c0d7804b6e67140b789fbd6e Mon Sep 17 00:00:00 2001 From: Kenan Date: Tue, 13 Feb 2024 15:30:51 +0000 Subject: [PATCH 07/36] victory-cursor-container --- demo/ts/components/create-container-demo.tsx | 4 +- .../victory-cursor-container-demo.tsx | 3 +- .../src/create-container-fn.tsx | 8 +- .../victory-cursor-container/src/index.tsx | 4 + .../src/victory-cursor-container-fn.tsx | 213 ++++++++++++++++++ 5 files changed, 227 insertions(+), 5 deletions(-) create mode 100644 packages/victory-cursor-container/src/victory-cursor-container-fn.tsx diff --git a/demo/ts/components/create-container-demo.tsx b/demo/ts/components/create-container-demo.tsx index 908c7634c..16114f937 100644 --- a/demo/ts/components/create-container-demo.tsx +++ b/demo/ts/components/create-container-demo.tsx @@ -274,9 +274,9 @@ class App extends React.Component { return (
- {/* + - */} + {/* */}
); } diff --git a/demo/ts/components/victory-cursor-container-demo.tsx b/demo/ts/components/victory-cursor-container-demo.tsx index 2ad25dd14..928bba28c 100644 --- a/demo/ts/components/victory-cursor-container-demo.tsx +++ b/demo/ts/components/victory-cursor-container-demo.tsx @@ -7,7 +7,8 @@ import { VictoryGroup } from "victory-group"; import { VictoryBar } from "victory-bar"; import { VictoryLine } from "victory-line"; import { VictoryScatter } from "victory-scatter"; -import { VictoryCursorContainer } from "victory-cursor-container"; +import { VictoryCursorContainerFn as VictoryCursorContainer } from "victory-cursor-container"; +// import { VictoryCursorContainer } from "victory-cursor-container"; import { VictoryTooltip } from "victory-tooltip"; import { VictoryLegend } from "victory-legend"; import { VictoryTheme, CoordinatesPropType } from "victory-core"; diff --git a/packages/victory-create-container/src/create-container-fn.tsx b/packages/victory-create-container/src/create-container-fn.tsx index e9b4a50d5..3b2eba5b6 100644 --- a/packages/victory-create-container/src/create-container-fn.tsx +++ b/packages/victory-create-container/src/create-container-fn.tsx @@ -13,6 +13,10 @@ import { VictoryVoronoiContainerFn, useVictoryVoronoiContainer, } from "victory-voronoi-container"; +import { + VictoryCursorContainerFn, + useVictoryCursorContainer, +} from "victory-cursor-container"; export type ContainerType = | "brush" @@ -100,8 +104,8 @@ const CONTAINERS: Record = { }, cursor: { name: "Cursor", - component: VictoryZoomContainerFn, - hook: useVictoryZoomContainer, + component: VictoryCursorContainerFn, + hook: useVictoryCursorContainer, }, voronoi: { name: "Voronoi", diff --git a/packages/victory-cursor-container/src/index.tsx b/packages/victory-cursor-container/src/index.tsx index 923c9954d..8d331974b 100644 --- a/packages/victory-cursor-container/src/index.tsx +++ b/packages/victory-cursor-container/src/index.tsx @@ -1,2 +1,6 @@ export * from "./cursor-helpers"; +export { + VictoryCursorContainerFn, + useVictoryCursorContainer, +} from "./victory-cursor-container-fn"; export * from "./victory-cursor-container"; diff --git a/packages/victory-cursor-container/src/victory-cursor-container-fn.tsx b/packages/victory-cursor-container/src/victory-cursor-container-fn.tsx new file mode 100644 index 000000000..91015e453 --- /dev/null +++ b/packages/victory-cursor-container/src/victory-cursor-container-fn.tsx @@ -0,0 +1,213 @@ +import React from "react"; +import { + Helpers, + VictoryContainerProps, + CoordinatesPropType, + VictoryLabelProps, + ValueOrAccessor, + VictoryLabel, + LineSegment, + VictoryContainerFn, +} from "victory-core"; +import { defaults, isObject } from "lodash"; +import { CursorHelpers } from "./cursor-helpers"; + +type Handler = ( + event: any, + targetProps: any, + eventKey?: any, + context?: any, +) => void; + +export type CursorCoordinatesPropType = CoordinatesPropType | number; + +export interface VictoryCursorContainerProps extends VictoryContainerProps { + cursorComponent?: React.ReactElement; + cursorDimension?: "x" | "y"; + cursorLabel?: ValueOrAccessor; + cursorLabelComponent?: React.ReactElement; + cursorLabelOffset?: CursorCoordinatesPropType; + defaultCursorValue?: CursorCoordinatesPropType; + disable?: boolean; + onCursorChange?: ( + value: CursorCoordinatesPropType, + props: VictoryCursorContainerProps, + ) => void; +} + +const defaultProps = { + cursorLabelComponent: , + cursorLabelOffset: { + x: 5, + y: -10, + }, + cursorComponent: , +}; + +export const useVictoryCursorContainer = ( + initialProps: VictoryCursorContainerProps, +) => { + const props = { ...defaultProps, ...initialProps }; + const { children } = props; + + const getCursorPosition = () => { + const { cursorValue, defaultCursorValue, domain, cursorDimension } = props; + if (cursorValue) { + return cursorValue; + } + + if (typeof defaultCursorValue === "number") { + return { + x: (domain.x[0] + domain.x[1]) / 2, + y: (domain.y[0] + domain.y[1]) / 2, + [cursorDimension]: defaultCursorValue, + }; + } + + return defaultCursorValue; + }; + + const getCursorLabelOffset = () => { + const { cursorLabelOffset } = props; + + if (typeof cursorLabelOffset === "number") { + return { + x: cursorLabelOffset, + y: cursorLabelOffset, + }; + } + + return cursorLabelOffset; + }; + + const getPadding = () => { + if (props.padding === undefined) { + const child = props.children.find((c) => { + return isObject(c.props) && c.props.padding !== undefined; + }); + return Helpers.getPadding(child.props); + } + return Helpers.getPadding(props); + }; + + const getCursorElements = () => { + // eslint-disable-line max-statements + const { + scale, + cursorLabelComponent, + cursorLabel, + cursorComponent, + width, + height, + name, + horizontal, + theme, + } = props; + const cursorDimension = CursorHelpers.getDimension(props); + const cursorValue = getCursorPosition(); + const cursorLabelOffset = getCursorLabelOffset(); + + if (!cursorValue) { + return []; + } + + const newElements: React.ReactElement[] = []; + const padding = getPadding(); + const cursorCoordinates = { + x: horizontal ? scale.y(cursorValue.y) : scale.x(cursorValue.x), + y: horizontal ? scale.x(cursorValue.x) : scale.y(cursorValue.y), + }; + if (cursorLabel) { + let labelProps = defaults({ active: true }, cursorLabelComponent.props, { + x: cursorCoordinates.x + cursorLabelOffset.x, + y: cursorCoordinates.y + cursorLabelOffset.y, + datum: cursorValue, + active: true, + key: `${name}-cursor-label`, + }); + if (Helpers.isTooltip(cursorLabelComponent)) { + const tooltipTheme = (theme && theme.tooltip) || {}; + labelProps = defaults({}, labelProps, tooltipTheme); + } + newElements.push( + React.cloneElement( + cursorLabelComponent, + defaults({}, labelProps, { + text: Helpers.evaluateProp(cursorLabel, labelProps), + }), + ), + ); + } + + const cursorStyle = Object.assign( + { stroke: "black" }, + cursorComponent.props.style, + ); + if (cursorDimension === "x" || cursorDimension === undefined) { + newElements.push( + React.cloneElement(cursorComponent, { + key: `${name}-x-cursor`, + x1: cursorCoordinates.x, + x2: cursorCoordinates.x, + y1: padding.top, + y2: height - padding.bottom, + style: cursorStyle, + }), + ); + } + if (cursorDimension === "y" || cursorDimension === undefined) { + newElements.push( + React.cloneElement(cursorComponent, { + key: `${name}-y-cursor`, + x1: padding.left, + x2: width - padding.right, + y1: cursorCoordinates.y, + y2: cursorCoordinates.y, + style: cursorStyle, + }), + ); + } + return newElements; + }; + + return { + props, + children: [ + ...React.Children.toArray(children), + ...getCursorElements(), + ] as React.ReactElement[], + }; +}; + +export const VictoryCursorContainerFn = ( + initialProps: VictoryCursorContainerProps, +) => { + const { props, children } = useVictoryCursorContainer(initialProps); + return {children}; +}; + +VictoryCursorContainerFn.role = "container"; + +VictoryCursorContainerFn.defaultEvents = ( + initialProps: VictoryCursorContainerProps, +) => { + const props = { ...defaultProps, ...initialProps }; + const createEventHandler = + (handler: Handler, disabled?: boolean): Handler => + // eslint-disable-next-line max-params + (event, targetProps, eventKey, context) => + disabled || props.disable + ? {} + : handler(event, { ...props, ...targetProps }, eventKey, context); + + return [ + { + target: "parent", + eventHandlers: { + onMouseLeave: createEventHandler(CursorHelpers.onMouseLeave), + onMouseMove: createEventHandler(CursorHelpers.onMouseMove), + onTouchMove: createEventHandler(CursorHelpers.onMouseMove), + }, + }, + ]; +}; From 9f9b9eee32013dedaf12cf2c0c045974533625b4 Mon Sep 17 00:00:00 2001 From: Kenan Date: Tue, 13 Feb 2024 15:52:08 +0000 Subject: [PATCH 08/36] victory-brush-container --- demo/ts/components/create-container-demo.tsx | 2 +- .../victory-brush-container-demo.tsx | 3 +- packages/victory-brush-container/src/index.ts | 4 + .../src/victory-brush-container-fn.tsx | 219 ++++++++++++++++++ .../src/create-container-fn.tsx | 9 +- 5 files changed, 232 insertions(+), 5 deletions(-) create mode 100644 packages/victory-brush-container/src/victory-brush-container-fn.tsx diff --git a/demo/ts/components/create-container-demo.tsx b/demo/ts/components/create-container-demo.tsx index 16114f937..16f3e1d38 100644 --- a/demo/ts/components/create-container-demo.tsx +++ b/demo/ts/components/create-container-demo.tsx @@ -276,7 +276,7 @@ class App extends React.Component { - {/* */} + ); } diff --git a/demo/ts/components/victory-brush-container-demo.tsx b/demo/ts/components/victory-brush-container-demo.tsx index 5dfd49fd7..ea8ab4e98 100644 --- a/demo/ts/components/victory-brush-container-demo.tsx +++ b/demo/ts/components/victory-brush-container-demo.tsx @@ -9,7 +9,8 @@ import { VictoryLine } from "victory-line"; import { VictoryScatter } from "victory-scatter"; import { VictoryLegend } from "victory-legend"; import { VictoryZoomContainer } from "victory-zoom-container"; -import { VictoryBrushContainer } from "victory-brush-container"; +import { VictoryBrushContainerFn as VictoryBrushContainer } from "victory-brush-container"; +// import { VictoryBrushContainer } from "victory-brush-container"; import { DomainTuple } from "victory-core"; interface VictoryBrushContainerDemoState { diff --git a/packages/victory-brush-container/src/index.ts b/packages/victory-brush-container/src/index.ts index 3ee89ad73..5743c2129 100644 --- a/packages/victory-brush-container/src/index.ts +++ b/packages/victory-brush-container/src/index.ts @@ -1,2 +1,6 @@ export * from "./victory-brush-container"; +export { + VictoryBrushContainerFn, + useVictoryBrushContainer, +} from "./victory-brush-container-fn"; export * from "./brush-helpers"; diff --git a/packages/victory-brush-container/src/victory-brush-container-fn.tsx b/packages/victory-brush-container/src/victory-brush-container-fn.tsx new file mode 100644 index 000000000..9757af169 --- /dev/null +++ b/packages/victory-brush-container/src/victory-brush-container-fn.tsx @@ -0,0 +1,219 @@ +import React from "react"; +import { + Selection, + Rect, + DomainTuple, + VictoryContainerProps, + VictoryContainerFn, +} from "victory-core"; +import { BrushHelpers } from "./brush-helpers"; +import { defaults } from "lodash"; +import isEqual from "react-fast-compare"; + +type Handler = ( + event: any, + targetProps: any, + eventKey?: any, + context?: any, +) => void; + +export interface VictoryBrushContainerProps extends VictoryContainerProps { + allowDrag?: boolean; + allowDraw?: boolean; + allowResize?: boolean; + brushComponent?: React.ReactElement; + brushDimension?: "x" | "y"; + brushDomain?: { x?: DomainTuple; y?: DomainTuple }; + brushStyle?: React.CSSProperties; + defaultBrushArea?: "all" | "none" | "disable" | "move"; + disable?: boolean; + handleComponent?: React.ReactElement; + handleStyle?: React.CSSProperties; + handleWidth?: number; + onBrushCleared?: ( + domain: { x: DomainTuple; y: DomainTuple }, + props: VictoryBrushContainerProps, + ) => void; + onBrushDomainChange?: ( + domain: { x: DomainTuple; y: DomainTuple }, + props: VictoryBrushContainerProps, + ) => void; + onBrushDomainChangeEnd?: ( + domain: { x: DomainTuple; y: DomainTuple }, + props: VictoryBrushContainerProps, + ) => void; +} + +const defaultProps = { + allowDrag: true, + allowDraw: true, + allowResize: true, + brushComponent: , + brushStyle: { + stroke: "transparent", + fill: "black", + fillOpacity: 0.1, + }, + handleComponent: , + handleStyle: { + stroke: "transparent", + fill: "transparent", + }, + handleWidth: 8, + mouseMoveThreshold: 0, +}; + +export const useVictoryBrushContainer = ( + initialProps: VictoryBrushContainerProps, +) => { + const props = { ...defaultProps, ...initialProps }; + const { children } = props; + + const getSelectBox = (coordinates) => { + const { x, y } = coordinates; + const { brushStyle, brushComponent, name } = props; + const brushComponentStyle = + brushComponent.props && brushComponent.props.style; + const cursor = !props.allowDrag && !props.allowResize ? "auto" : "move"; + return x[0] !== x[1] && y[0] !== y[1] + ? React.cloneElement(brushComponent, { + key: `${name}-brush`, + width: Math.abs(x[1] - x[0]) || 1, + height: Math.abs(y[1] - y[0]) || 1, + x: Math.min(x[0], x[1]), + y: Math.min(y[0], y[1]), + cursor, + style: defaults({}, brushComponentStyle, brushStyle), + }) + : null; + }; + + const getCursorPointers = () => { + const cursors = { + yProps: "ns-resize", + xProps: "ew-resize", + }; + if (!props.allowResize && props.allowDrag) { + cursors.xProps = "move"; + cursors.yProps = "move"; + } else if (!props.allowResize && !props.allowDrag) { + cursors.xProps = "auto"; + cursors.yProps = "auto"; + } + return cursors; + }; + + const getHandles = (domain) => { + const { handleWidth, handleStyle, handleComponent, name } = props; + const domainBox = BrushHelpers.getDomainBox(props, domain); + const { x1, x2, y1, y2 } = domainBox; + const { top, bottom, left, right } = BrushHelpers.getHandles( + props, + domainBox, + ); + const width = Math.abs(x2 - x1) || 1; + const height = Math.abs(y2 - y1) || 1; + const handleComponentStyle = + (handleComponent.props && handleComponent.props.style) || {}; + const style = defaults({}, handleComponentStyle, handleStyle); + + const cursors = getCursorPointers(); + const yProps = { + style, + width, + height: handleWidth, + cursor: cursors.yProps, + }; + const xProps = { + style, + width: handleWidth, + height, + cursor: cursors.xProps, + }; + + const handleProps = { + top: top && Object.assign({ x: top.x1, y: top.y1 }, yProps), + bottom: bottom && Object.assign({ x: bottom.x1, y: bottom.y1 }, yProps), + left: left && Object.assign({ y: left.y1, x: left.x1 }, xProps), + right: right && Object.assign({ y: right.y1, x: right.x1 }, xProps), + }; + const handles = ["top", "bottom", "left", "right"].reduce( + (memo, curr) => + handleProps[curr] + ? memo.concat( + React.cloneElement( + handleComponent, + Object.assign( + { key: `${name}-handle-${curr}` }, + handleProps[curr], + ), + ), + ) + : memo, + [] as React.ReactElement[], + ); + return handles.length ? handles : null; + }; + + const getRect = () => { + const { currentDomain, cachedBrushDomain } = props; + const brushDomain = defaults({}, props.brushDomain, props.domain); + const domain = isEqual(brushDomain, cachedBrushDomain) + ? defaults({}, currentDomain, brushDomain) + : brushDomain; + const coordinates = Selection.getDomainCoordinates(props, domain); + const selectBox = getSelectBox(coordinates); + return selectBox ? [selectBox, getHandles(domain)] : []; + }; + + return { + props, + children: [ + ...React.Children.toArray(children), + ...getRect(), + ] as React.ReactElement[], + }; +}; + +export const VictoryBrushContainerFn = ( + initialProps: VictoryBrushContainerProps, +) => { + const { props, children } = useVictoryBrushContainer(initialProps); + return {children}; +}; + +VictoryBrushContainerFn.role = "container"; + +VictoryBrushContainerFn.defaultEvents = ( + initialProps: VictoryBrushContainerProps, +) => { + const props = { ...defaultProps, ...initialProps }; + const createEventHandler = + (handler: Handler, isDisabled?: (targetProps: any) => boolean): Handler => + // eslint-disable-next-line max-params + (event, targetProps, eventKey, context) => + props.disable || isDisabled?.(targetProps) + ? {} + : handler(event, { ...props, ...targetProps }, eventKey, context); + + return [ + { + target: "parent", + eventHandlers: { + onMouseDown: createEventHandler(BrushHelpers.onMouseDown), + onTouchStart: createEventHandler(BrushHelpers.onMouseDown), + onGlobalMouseMove: createEventHandler( + BrushHelpers.onGlobalMouseMove, + (targetProps) => !targetProps.isPanning && !targetProps.isSelecting, + ), + onGlobalTouchMove: createEventHandler( + BrushHelpers.onGlobalMouseMove, + (targetProps) => !targetProps.isPanning && !targetProps.isSelecting, + ), + onGlobalMouseUp: createEventHandler(BrushHelpers.onGlobalMouseUp), + onGlobalTouchEnd: createEventHandler(BrushHelpers.onGlobalMouseUp), + onGlobalTouchCancel: createEventHandler(BrushHelpers.onGlobalMouseUp), + }, + }, + ]; +}; diff --git a/packages/victory-create-container/src/create-container-fn.tsx b/packages/victory-create-container/src/create-container-fn.tsx index 3b2eba5b6..094558d62 100644 --- a/packages/victory-create-container/src/create-container-fn.tsx +++ b/packages/victory-create-container/src/create-container-fn.tsx @@ -17,6 +17,10 @@ import { VictoryCursorContainerFn, useVictoryCursorContainer, } from "victory-cursor-container"; +import { + VictoryBrushContainerFn, + useVictoryBrushContainer, +} from "victory-brush-container"; export type ContainerType = | "brush" @@ -85,7 +89,6 @@ type Container = { }; }; -// TODO: Add correct components & hooks when they are built const CONTAINERS: Record = { zoom: { name: "Zoom", @@ -99,8 +102,8 @@ const CONTAINERS: Record = { }, brush: { name: "Brush", - component: VictoryZoomContainerFn, - hook: useVictoryZoomContainer, + component: VictoryBrushContainerFn, + hook: useVictoryBrushContainer, }, cursor: { name: "Cursor", From f791d3661d4f50debcb39e46e4c8d7dea2dc78fa Mon Sep 17 00:00:00 2001 From: Kenan Date: Tue, 13 Feb 2024 16:03:44 +0000 Subject: [PATCH 09/36] Remove class files --- packages/victory-brush-container/src/index.ts | 4 - .../src/victory-brush-container-fn.tsx | 219 ---------- .../src/victory-brush-container.tsx | 343 ++++++++------- .../src/create-container.ts | 173 -------- ...-container-fn.tsx => create-container.tsx} | 20 +- .../victory-create-container/src/index.ts | 1 - .../victory-cursor-container/src/index.tsx | 4 - .../src/victory-cursor-container-fn.tsx | 213 --------- .../src/victory-cursor-container.tsx | 333 ++++++++------- .../victory-selection-container/src/index.ts | 4 - .../src/victory-selection-container-fn.tsx | 117 ----- .../src/victory-selection-container.tsx | 163 ++++--- .../victory-voronoi-container/src/index.ts | 4 - .../src/victory-voronoi-container-fn.tsx | 242 ----------- .../src/victory-voronoi-container.tsx | 404 +++++++++--------- packages/victory-zoom-container/src/index.ts | 4 - .../src/victory-zoom-container-fn.tsx | 226 ---------- .../src/victory-zoom-container.tsx | 380 ++++++++-------- 18 files changed, 807 insertions(+), 2047 deletions(-) delete mode 100644 packages/victory-brush-container/src/victory-brush-container-fn.tsx delete mode 100644 packages/victory-create-container/src/create-container.ts rename packages/victory-create-container/src/{create-container-fn.tsx => create-container.tsx} (92%) delete mode 100644 packages/victory-cursor-container/src/victory-cursor-container-fn.tsx delete mode 100644 packages/victory-selection-container/src/victory-selection-container-fn.tsx delete mode 100644 packages/victory-voronoi-container/src/victory-voronoi-container-fn.tsx delete mode 100644 packages/victory-zoom-container/src/victory-zoom-container-fn.tsx diff --git a/packages/victory-brush-container/src/index.ts b/packages/victory-brush-container/src/index.ts index 5743c2129..3ee89ad73 100644 --- a/packages/victory-brush-container/src/index.ts +++ b/packages/victory-brush-container/src/index.ts @@ -1,6 +1,2 @@ export * from "./victory-brush-container"; -export { - VictoryBrushContainerFn, - useVictoryBrushContainer, -} from "./victory-brush-container-fn"; export * from "./brush-helpers"; diff --git a/packages/victory-brush-container/src/victory-brush-container-fn.tsx b/packages/victory-brush-container/src/victory-brush-container-fn.tsx deleted file mode 100644 index 9757af169..000000000 --- a/packages/victory-brush-container/src/victory-brush-container-fn.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import React from "react"; -import { - Selection, - Rect, - DomainTuple, - VictoryContainerProps, - VictoryContainerFn, -} from "victory-core"; -import { BrushHelpers } from "./brush-helpers"; -import { defaults } from "lodash"; -import isEqual from "react-fast-compare"; - -type Handler = ( - event: any, - targetProps: any, - eventKey?: any, - context?: any, -) => void; - -export interface VictoryBrushContainerProps extends VictoryContainerProps { - allowDrag?: boolean; - allowDraw?: boolean; - allowResize?: boolean; - brushComponent?: React.ReactElement; - brushDimension?: "x" | "y"; - brushDomain?: { x?: DomainTuple; y?: DomainTuple }; - brushStyle?: React.CSSProperties; - defaultBrushArea?: "all" | "none" | "disable" | "move"; - disable?: boolean; - handleComponent?: React.ReactElement; - handleStyle?: React.CSSProperties; - handleWidth?: number; - onBrushCleared?: ( - domain: { x: DomainTuple; y: DomainTuple }, - props: VictoryBrushContainerProps, - ) => void; - onBrushDomainChange?: ( - domain: { x: DomainTuple; y: DomainTuple }, - props: VictoryBrushContainerProps, - ) => void; - onBrushDomainChangeEnd?: ( - domain: { x: DomainTuple; y: DomainTuple }, - props: VictoryBrushContainerProps, - ) => void; -} - -const defaultProps = { - allowDrag: true, - allowDraw: true, - allowResize: true, - brushComponent: , - brushStyle: { - stroke: "transparent", - fill: "black", - fillOpacity: 0.1, - }, - handleComponent: , - handleStyle: { - stroke: "transparent", - fill: "transparent", - }, - handleWidth: 8, - mouseMoveThreshold: 0, -}; - -export const useVictoryBrushContainer = ( - initialProps: VictoryBrushContainerProps, -) => { - const props = { ...defaultProps, ...initialProps }; - const { children } = props; - - const getSelectBox = (coordinates) => { - const { x, y } = coordinates; - const { brushStyle, brushComponent, name } = props; - const brushComponentStyle = - brushComponent.props && brushComponent.props.style; - const cursor = !props.allowDrag && !props.allowResize ? "auto" : "move"; - return x[0] !== x[1] && y[0] !== y[1] - ? React.cloneElement(brushComponent, { - key: `${name}-brush`, - width: Math.abs(x[1] - x[0]) || 1, - height: Math.abs(y[1] - y[0]) || 1, - x: Math.min(x[0], x[1]), - y: Math.min(y[0], y[1]), - cursor, - style: defaults({}, brushComponentStyle, brushStyle), - }) - : null; - }; - - const getCursorPointers = () => { - const cursors = { - yProps: "ns-resize", - xProps: "ew-resize", - }; - if (!props.allowResize && props.allowDrag) { - cursors.xProps = "move"; - cursors.yProps = "move"; - } else if (!props.allowResize && !props.allowDrag) { - cursors.xProps = "auto"; - cursors.yProps = "auto"; - } - return cursors; - }; - - const getHandles = (domain) => { - const { handleWidth, handleStyle, handleComponent, name } = props; - const domainBox = BrushHelpers.getDomainBox(props, domain); - const { x1, x2, y1, y2 } = domainBox; - const { top, bottom, left, right } = BrushHelpers.getHandles( - props, - domainBox, - ); - const width = Math.abs(x2 - x1) || 1; - const height = Math.abs(y2 - y1) || 1; - const handleComponentStyle = - (handleComponent.props && handleComponent.props.style) || {}; - const style = defaults({}, handleComponentStyle, handleStyle); - - const cursors = getCursorPointers(); - const yProps = { - style, - width, - height: handleWidth, - cursor: cursors.yProps, - }; - const xProps = { - style, - width: handleWidth, - height, - cursor: cursors.xProps, - }; - - const handleProps = { - top: top && Object.assign({ x: top.x1, y: top.y1 }, yProps), - bottom: bottom && Object.assign({ x: bottom.x1, y: bottom.y1 }, yProps), - left: left && Object.assign({ y: left.y1, x: left.x1 }, xProps), - right: right && Object.assign({ y: right.y1, x: right.x1 }, xProps), - }; - const handles = ["top", "bottom", "left", "right"].reduce( - (memo, curr) => - handleProps[curr] - ? memo.concat( - React.cloneElement( - handleComponent, - Object.assign( - { key: `${name}-handle-${curr}` }, - handleProps[curr], - ), - ), - ) - : memo, - [] as React.ReactElement[], - ); - return handles.length ? handles : null; - }; - - const getRect = () => { - const { currentDomain, cachedBrushDomain } = props; - const brushDomain = defaults({}, props.brushDomain, props.domain); - const domain = isEqual(brushDomain, cachedBrushDomain) - ? defaults({}, currentDomain, brushDomain) - : brushDomain; - const coordinates = Selection.getDomainCoordinates(props, domain); - const selectBox = getSelectBox(coordinates); - return selectBox ? [selectBox, getHandles(domain)] : []; - }; - - return { - props, - children: [ - ...React.Children.toArray(children), - ...getRect(), - ] as React.ReactElement[], - }; -}; - -export const VictoryBrushContainerFn = ( - initialProps: VictoryBrushContainerProps, -) => { - const { props, children } = useVictoryBrushContainer(initialProps); - return {children}; -}; - -VictoryBrushContainerFn.role = "container"; - -VictoryBrushContainerFn.defaultEvents = ( - initialProps: VictoryBrushContainerProps, -) => { - const props = { ...defaultProps, ...initialProps }; - const createEventHandler = - (handler: Handler, isDisabled?: (targetProps: any) => boolean): Handler => - // eslint-disable-next-line max-params - (event, targetProps, eventKey, context) => - props.disable || isDisabled?.(targetProps) - ? {} - : handler(event, { ...props, ...targetProps }, eventKey, context); - - return [ - { - target: "parent", - eventHandlers: { - onMouseDown: createEventHandler(BrushHelpers.onMouseDown), - onTouchStart: createEventHandler(BrushHelpers.onMouseDown), - onGlobalMouseMove: createEventHandler( - BrushHelpers.onGlobalMouseMove, - (targetProps) => !targetProps.isPanning && !targetProps.isSelecting, - ), - onGlobalTouchMove: createEventHandler( - BrushHelpers.onGlobalMouseMove, - (targetProps) => !targetProps.isPanning && !targetProps.isSelecting, - ), - onGlobalMouseUp: createEventHandler(BrushHelpers.onGlobalMouseUp), - onGlobalTouchEnd: createEventHandler(BrushHelpers.onGlobalMouseUp), - onGlobalTouchCancel: createEventHandler(BrushHelpers.onGlobalMouseUp), - }, - }, - ]; -}; diff --git a/packages/victory-brush-container/src/victory-brush-container.tsx b/packages/victory-brush-container/src/victory-brush-container.tsx index 455897d2f..dc9be5ace 100644 --- a/packages/victory-brush-container/src/victory-brush-container.tsx +++ b/packages/victory-brush-container/src/victory-brush-container.tsx @@ -1,15 +1,22 @@ import React from "react"; import { - VictoryContainer, Selection, Rect, DomainTuple, VictoryContainerProps, + VictoryContainerFn, } from "victory-core"; import { BrushHelpers } from "./brush-helpers"; import { defaults } from "lodash"; import isEqual from "react-fast-compare"; +type Handler = ( + event: any, + targetProps: any, + eventKey?: any, + context?: any, +) => void; + export interface VictoryBrushContainerProps extends VictoryContainerProps { allowDrag?: boolean; allowDraw?: boolean; @@ -37,186 +44,176 @@ export interface VictoryBrushContainerProps extends VictoryContainerProps { ) => void; } -type ComponentClass = { new (props: TProps): React.Component }; - -export function brushContainerMixin< - TBase extends ComponentClass, - TProps extends VictoryBrushContainerProps, ->(Base: TBase) { - // @ts-expect-error "TS2545: A mixin class must have a constructor with a single rest parameter of type 'any[]'." - return class VictoryBrushContainer extends Base { - static displayName = "VictoryBrushContainer"; - static defaultProps = { - ...VictoryContainer.defaultProps, - allowDrag: true, - allowDraw: true, - allowResize: true, - brushComponent: , - brushStyle: { - stroke: "transparent", - fill: "black", - fillOpacity: 0.1, - }, - handleComponent: , - handleStyle: { - stroke: "transparent", - fill: "transparent", - }, - handleWidth: 8, - mouseMoveThreshold: 0, - }; +const defaultProps = { + allowDrag: true, + allowDraw: true, + allowResize: true, + brushComponent: , + brushStyle: { + stroke: "transparent", + fill: "black", + fillOpacity: 0.1, + }, + handleComponent: , + handleStyle: { + stroke: "transparent", + fill: "transparent", + }, + handleWidth: 8, + mouseMoveThreshold: 0, +}; - static defaultEvents = (props) => { - return [ - { - target: "parent", - eventHandlers: { - onMouseDown: (evt, targetProps) => { - return props.disable - ? {} - : BrushHelpers.onMouseDown(evt, targetProps); - }, - onTouchStart: (evt, targetProps) => { - return props.disable - ? {} - : BrushHelpers.onMouseDown(evt, targetProps); - }, - onGlobalMouseMove: (evt, targetProps) => { - return props.disable || - (!targetProps.isPanning && !targetProps.isSelecting) - ? {} - : BrushHelpers.onGlobalMouseMove(evt, targetProps); - }, - onGlobalTouchMove: (evt, targetProps) => { - return props.disable || - (!targetProps.isPanning && !targetProps.isSelecting) - ? {} - : BrushHelpers.onGlobalMouseMove(evt, targetProps); - }, - onGlobalMouseUp: (evt, targetProps) => { - return props.disable - ? {} - : BrushHelpers.onGlobalMouseUp(evt, targetProps); - }, - onGlobalTouchEnd: (evt, targetProps) => { - return props.disable - ? {} - : BrushHelpers.onGlobalMouseUp(evt, targetProps); - }, - onGlobalTouchCancel: (evt, targetProps) => { - return props.disable - ? {} - : BrushHelpers.onGlobalMouseUp(evt, targetProps); - }, - }, - }, - ]; - }; +export const useVictoryBrushContainer = ( + initialProps: VictoryBrushContainerProps, +) => { + const props = { ...defaultProps, ...initialProps }; + const { children } = props; - getSelectBox(props, coordinates) { - const { x, y } = coordinates; - const { brushStyle, brushComponent, name } = props; - const brushComponentStyle = - brushComponent.props && brushComponent.props.style; - const cursor = !props.allowDrag && !props.allowResize ? "auto" : "move"; - return x[0] !== x[1] && y[0] !== y[1] - ? React.cloneElement(brushComponent, { - key: `${name}-brush`, - width: Math.abs(x[1] - x[0]) || 1, - height: Math.abs(y[1] - y[0]) || 1, - x: Math.min(x[0], x[1]), - y: Math.min(y[0], y[1]), - cursor, - style: defaults({}, brushComponentStyle, brushStyle), - }) - : null; - } + const getSelectBox = (coordinates) => { + const { x, y } = coordinates; + const { brushStyle, brushComponent, name } = props; + const brushComponentStyle = + brushComponent.props && brushComponent.props.style; + const cursor = !props.allowDrag && !props.allowResize ? "auto" : "move"; + return x[0] !== x[1] && y[0] !== y[1] + ? React.cloneElement(brushComponent, { + key: `${name}-brush`, + width: Math.abs(x[1] - x[0]) || 1, + height: Math.abs(y[1] - y[0]) || 1, + x: Math.min(x[0], x[1]), + y: Math.min(y[0], y[1]), + cursor, + style: defaults({}, brushComponentStyle, brushStyle), + }) + : null; + }; - getCursorPointers(props) { - const cursors = { - yProps: "ns-resize", - xProps: "ew-resize", - }; - if (!props.allowResize && props.allowDrag) { - cursors.xProps = "move"; - cursors.yProps = "move"; - } else if (!props.allowResize && !props.allowDrag) { - cursors.xProps = "auto"; - cursors.yProps = "auto"; - } - return cursors; + const getCursorPointers = () => { + const cursors = { + yProps: "ns-resize", + xProps: "ew-resize", + }; + if (!props.allowResize && props.allowDrag) { + cursors.xProps = "move"; + cursors.yProps = "move"; + } else if (!props.allowResize && !props.allowDrag) { + cursors.xProps = "auto"; + cursors.yProps = "auto"; } + return cursors; + }; - getHandles(props, domain) { - const { handleWidth, handleStyle, handleComponent, name } = props; - const domainBox = BrushHelpers.getDomainBox(props, domain); - const { x1, x2, y1, y2 } = domainBox; - const { top, bottom, left, right } = BrushHelpers.getHandles( - props, - domainBox, - ); - const width = Math.abs(x2 - x1) || 1; - const height = Math.abs(y2 - y1) || 1; - const handleComponentStyle = - (handleComponent.props && handleComponent.props.style) || {}; - const style = defaults({}, handleComponentStyle, handleStyle); + const getHandles = (domain) => { + const { handleWidth, handleStyle, handleComponent, name } = props; + const domainBox = BrushHelpers.getDomainBox(props, domain); + const { x1, x2, y1, y2 } = domainBox; + const { top, bottom, left, right } = BrushHelpers.getHandles( + props, + domainBox, + ); + const width = Math.abs(x2 - x1) || 1; + const height = Math.abs(y2 - y1) || 1; + const handleComponentStyle = + (handleComponent.props && handleComponent.props.style) || {}; + const style = defaults({}, handleComponentStyle, handleStyle); - const cursors = this.getCursorPointers(props); - const yProps = { - style, - width, - height: handleWidth, - cursor: cursors.yProps, - }; - const xProps = { - style, - width: handleWidth, - height, - cursor: cursors.xProps, - }; + const cursors = getCursorPointers(); + const yProps = { + style, + width, + height: handleWidth, + cursor: cursors.yProps, + }; + const xProps = { + style, + width: handleWidth, + height, + cursor: cursors.xProps, + }; - const handleProps = { - top: top && Object.assign({ x: top.x1, y: top.y1 }, yProps), - bottom: bottom && Object.assign({ x: bottom.x1, y: bottom.y1 }, yProps), - left: left && Object.assign({ y: left.y1, x: left.x1 }, xProps), - right: right && Object.assign({ y: right.y1, x: right.x1 }, xProps), - }; - const handles = ["top", "bottom", "left", "right"].reduce( - (memo, curr) => - handleProps[curr] - ? memo.concat( - React.cloneElement( - handleComponent, - Object.assign( - { key: `${name}-handle-${curr}` }, - handleProps[curr], - ), + const handleProps = { + top: top && Object.assign({ x: top.x1, y: top.y1 }, yProps), + bottom: bottom && Object.assign({ x: bottom.x1, y: bottom.y1 }, yProps), + left: left && Object.assign({ y: left.y1, x: left.x1 }, xProps), + right: right && Object.assign({ y: right.y1, x: right.x1 }, xProps), + }; + const handles = ["top", "bottom", "left", "right"].reduce( + (memo, curr) => + handleProps[curr] + ? memo.concat( + React.cloneElement( + handleComponent, + Object.assign( + { key: `${name}-handle-${curr}` }, + handleProps[curr], ), - ) - : memo, - [] as React.ReactElement[], - ); - return handles.length ? handles : null; - } + ), + ) + : memo, + [] as React.ReactElement[], + ); + return handles.length ? handles : null; + }; - getRect(props) { - const { currentDomain, cachedBrushDomain } = props; - const brushDomain = defaults({}, props.brushDomain, props.domain); - const domain = isEqual(brushDomain, cachedBrushDomain) - ? defaults({}, currentDomain, brushDomain) - : brushDomain; - const coordinates = Selection.getDomainCoordinates(props, domain); - const selectBox = this.getSelectBox(props, coordinates); - return selectBox ? [selectBox, this.getHandles(props, domain)] : []; - } + const getRect = () => { + const { currentDomain, cachedBrushDomain } = props; + const brushDomain = defaults({}, props.brushDomain, props.domain); + const domain = isEqual(brushDomain, cachedBrushDomain) + ? defaults({}, currentDomain, brushDomain) + : brushDomain; + const coordinates = Selection.getDomainCoordinates(props, domain); + const selectBox = getSelectBox(coordinates); + return selectBox ? [selectBox, getHandles(domain)] : []; + }; - // Overrides method in VictoryContainer - getChildren(props) { - return [ - ...React.Children.toArray(props.children), - ...this.getRect(props), - ]; - } + return { + props, + children: [ + ...React.Children.toArray(children), + ...getRect(), + ] as React.ReactElement[], }; -} -export const VictoryBrushContainer = brushContainerMixin(VictoryContainer); +}; + +export const VictoryBrushContainer = ( + initialProps: VictoryBrushContainerProps, +) => { + const { props, children } = useVictoryBrushContainer(initialProps); + return {children}; +}; + +VictoryBrushContainer.role = "container"; + +VictoryBrushContainer.defaultEvents = ( + initialProps: VictoryBrushContainerProps, +) => { + const props = { ...defaultProps, ...initialProps }; + const createEventHandler = + (handler: Handler, isDisabled?: (targetProps: any) => boolean): Handler => + // eslint-disable-next-line max-params + (event, targetProps, eventKey, context) => + props.disable || isDisabled?.(targetProps) + ? {} + : handler(event, { ...props, ...targetProps }, eventKey, context); + + return [ + { + target: "parent", + eventHandlers: { + onMouseDown: createEventHandler(BrushHelpers.onMouseDown), + onTouchStart: createEventHandler(BrushHelpers.onMouseDown), + onGlobalMouseMove: createEventHandler( + BrushHelpers.onGlobalMouseMove, + (targetProps) => !targetProps.isPanning && !targetProps.isSelecting, + ), + onGlobalTouchMove: createEventHandler( + BrushHelpers.onGlobalMouseMove, + (targetProps) => !targetProps.isPanning && !targetProps.isSelecting, + ), + onGlobalMouseUp: createEventHandler(BrushHelpers.onGlobalMouseUp), + onGlobalTouchEnd: createEventHandler(BrushHelpers.onGlobalMouseUp), + onGlobalTouchCancel: createEventHandler(BrushHelpers.onGlobalMouseUp), + }, + }, + ]; +}; diff --git a/packages/victory-create-container/src/create-container.ts b/packages/victory-create-container/src/create-container.ts deleted file mode 100644 index e0c3f739e..000000000 --- a/packages/victory-create-container/src/create-container.ts +++ /dev/null @@ -1,173 +0,0 @@ -import React from "react"; -import { toPairs, groupBy, forOwn, includes, flow, isEmpty } from "lodash"; -import { Helpers, VictoryContainer, Log } from "victory-core"; -import { voronoiContainerMixin } from "victory-voronoi-container"; -import { zoomContainerMixin } from "victory-zoom-container"; -import { selectionContainerMixin } from "victory-selection-container"; -import { brushContainerMixin } from "victory-brush-container"; -import { cursorContainerMixin } from "victory-cursor-container"; - -export type ContainerType = - | "brush" - | "cursor" - | "selection" - | "voronoi" - | "zoom"; - -type MixinFunction = (...args: any[]) => any; - -function ensureArray(thing: T): [] | T | T[] { - if (!thing) { - return []; - } else if (!Array.isArray(thing)) { - return [thing]; - } - return thing; -} - -const combineEventHandlers = (eventHandlersArray: any[]) => { - // takes an array of event handler objects and produces one eventHandlers object - // creates a custom combinedHandler() for events with multiple conflicting handlers - return eventHandlersArray.reduce((localHandlers, finalHandlers) => { - forOwn(localHandlers, (localHandler, eventName) => { - const existingHandler = finalHandlers[eventName]; - if (existingHandler) { - // create new handler for event that concats the existing handler's mutations with new ones - finalHandlers[eventName] = function combinedHandler(...params) { - // named for debug clarity - // sometimes handlers return undefined; use empty array instead, for concat() - const existingMutations = ensureArray(existingHandler(...params)); - const localMutations = ensureArray(localHandler(...params)); - return existingMutations.concat(localMutations); - }; - } else { - finalHandlers[eventName] = localHandler; - } - }); - return finalHandlers; - }); -}; - -const combineDefaultEvents = (defaultEvents: any[]) => { - // takes a defaultEvents array and returns one equal or lesser length, - // by combining any events that have the same target - const eventsByTarget = groupBy(defaultEvents, "target"); - const events = toPairs(eventsByTarget).map(([target, eventsArray]) => { - const newEventsArray = eventsArray.filter(Boolean); - return isEmpty(newEventsArray) - ? null - : { - target, - eventHandlers: combineEventHandlers( - eventsArray.map((event) => event.eventHandlers), - ), - // note: does not currently handle eventKey or childName - }; - }); - return events.filter(Boolean); -}; - -export const combineContainerMixins = ( - mixins: MixinFunction[], - Container: React.ComponentType, -) => { - // similar to Object.assign(A, B), this function will decide conflicts in favor mixinB. - // this applies to propTypes and defaultProps. - // getChildren will call A's getChildren() and pass the resulting children to B's. - // defaultEvents attempts to resolve any conflicts between A and B's defaultEvents. - const Classes = mixins.map((mixin) => mixin(Container)); - const instances = Classes.map((Class) => new Class()); - const NaiveCombinedContainer = flow(mixins)(Container); - - const displayType = Classes.map((Class) => { - const match = Class.displayName.match(/Victory(.*)Container/); - return match[1] || ""; - }).join(""); - - return class VictoryCombinedContainer extends NaiveCombinedContainer { - static displayName = `Victory${displayType}Container`; - - static propTypes = Classes.reduce( - (propTypes, Class) => ({ ...propTypes, ...Class.propTypes }), - {}, - ); - - static defaultProps = Classes.reduce( - (defaultProps, Class) => ({ ...defaultProps, ...Class.defaultProps }), - {}, - ); - - static defaultEvents = (props) => { - return combineDefaultEvents( - Classes.reduce((defaultEvents, Class) => { - const events = Helpers.isFunction(Class.defaultEvents) - ? Class.defaultEvents(props) - : Class.defaultEvents; - return [...defaultEvents, ...events]; - }, []), - ); - }; - - getChildren(props) { - return instances.reduce( - (children, instance) => instance.getChildren({ ...props, children }), - props.children, - ); - } - }; -}; - -const checkBehaviorName = ( - behavior: ContainerType, - behaviors: ContainerType[], -) => { - if (behavior && !includes(behaviors, behavior)) { - Log.warn( - `"${behavior}" is not a valid behavior. Choose from [${behaviors.join( - ", ", - )}].`, - ); - } -}; - -export const makeCreateContainerFunction = - ( - mixinMap: Record, - Container: React.ComponentType, - ) => - ( - behaviorA: ContainerType, - behaviorB: ContainerType, - ...invalid: ContainerType[] - ) => { - const behaviors = Object.keys(mixinMap) as ContainerType[]; - - checkBehaviorName(behaviorA, behaviors); - checkBehaviorName(behaviorB, behaviors); - - if (invalid.length) { - Log.warn( - "too many arguments given to createContainer (maximum accepted: 2).", - ); - } - - const firstMixins = mixinMap[behaviorA]; - const secondMixins = mixinMap[behaviorB] || []; - - if (!firstMixins) { - return Container; - } - - return combineContainerMixins([...firstMixins, ...secondMixins], Container); - }; - -export const createContainer = makeCreateContainerFunction( - { - zoom: [zoomContainerMixin], - voronoi: [voronoiContainerMixin], - selection: [selectionContainerMixin], - cursor: [cursorContainerMixin], - brush: [brushContainerMixin], - }, - VictoryContainer, -); diff --git a/packages/victory-create-container/src/create-container-fn.tsx b/packages/victory-create-container/src/create-container.tsx similarity index 92% rename from packages/victory-create-container/src/create-container-fn.tsx rename to packages/victory-create-container/src/create-container.tsx index 094558d62..f105bbeb4 100644 --- a/packages/victory-create-container/src/create-container-fn.tsx +++ b/packages/victory-create-container/src/create-container.tsx @@ -1,24 +1,24 @@ import { - VictoryZoomContainerFn, + VictoryZoomContainer, useVictoryZoomContainer, } from "victory-zoom-container"; import { - VictorySelectionContainerFn, + VictorySelectionContainer, useVictorySelectionContainer, } from "victory-selection-container"; import React from "react"; import { VictoryContainerFn } from "victory-core"; import { forOwn, groupBy, isEmpty, toPairs } from "lodash"; import { - VictoryVoronoiContainerFn, + VictoryVoronoiContainer, useVictoryVoronoiContainer, } from "victory-voronoi-container"; import { - VictoryCursorContainerFn, + VictoryCursorContainer, useVictoryCursorContainer, } from "victory-cursor-container"; import { - VictoryBrushContainerFn, + VictoryBrushContainer, useVictoryBrushContainer, } from "victory-brush-container"; @@ -92,27 +92,27 @@ type Container = { const CONTAINERS: Record = { zoom: { name: "Zoom", - component: VictoryZoomContainerFn, + component: VictoryZoomContainer, hook: useVictoryZoomContainer, }, selection: { name: "Selection", - component: VictorySelectionContainerFn, + component: VictorySelectionContainer, hook: useVictorySelectionContainer, }, brush: { name: "Brush", - component: VictoryBrushContainerFn, + component: VictoryBrushContainer, hook: useVictoryBrushContainer, }, cursor: { name: "Cursor", - component: VictoryCursorContainerFn, + component: VictoryCursorContainer, hook: useVictoryCursorContainer, }, voronoi: { name: "Voronoi", - component: VictoryVoronoiContainerFn, + component: VictoryVoronoiContainer, hook: useVictoryVoronoiContainer, }, }; diff --git a/packages/victory-create-container/src/index.ts b/packages/victory-create-container/src/index.ts index a7c0a1c7b..5f9b97723 100644 --- a/packages/victory-create-container/src/index.ts +++ b/packages/victory-create-container/src/index.ts @@ -1,2 +1 @@ export * from "./create-container"; -export { createContainerFn } from "./create-container-fn"; diff --git a/packages/victory-cursor-container/src/index.tsx b/packages/victory-cursor-container/src/index.tsx index 8d331974b..923c9954d 100644 --- a/packages/victory-cursor-container/src/index.tsx +++ b/packages/victory-cursor-container/src/index.tsx @@ -1,6 +1,2 @@ export * from "./cursor-helpers"; -export { - VictoryCursorContainerFn, - useVictoryCursorContainer, -} from "./victory-cursor-container-fn"; export * from "./victory-cursor-container"; diff --git a/packages/victory-cursor-container/src/victory-cursor-container-fn.tsx b/packages/victory-cursor-container/src/victory-cursor-container-fn.tsx deleted file mode 100644 index 91015e453..000000000 --- a/packages/victory-cursor-container/src/victory-cursor-container-fn.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import React from "react"; -import { - Helpers, - VictoryContainerProps, - CoordinatesPropType, - VictoryLabelProps, - ValueOrAccessor, - VictoryLabel, - LineSegment, - VictoryContainerFn, -} from "victory-core"; -import { defaults, isObject } from "lodash"; -import { CursorHelpers } from "./cursor-helpers"; - -type Handler = ( - event: any, - targetProps: any, - eventKey?: any, - context?: any, -) => void; - -export type CursorCoordinatesPropType = CoordinatesPropType | number; - -export interface VictoryCursorContainerProps extends VictoryContainerProps { - cursorComponent?: React.ReactElement; - cursorDimension?: "x" | "y"; - cursorLabel?: ValueOrAccessor; - cursorLabelComponent?: React.ReactElement; - cursorLabelOffset?: CursorCoordinatesPropType; - defaultCursorValue?: CursorCoordinatesPropType; - disable?: boolean; - onCursorChange?: ( - value: CursorCoordinatesPropType, - props: VictoryCursorContainerProps, - ) => void; -} - -const defaultProps = { - cursorLabelComponent: , - cursorLabelOffset: { - x: 5, - y: -10, - }, - cursorComponent: , -}; - -export const useVictoryCursorContainer = ( - initialProps: VictoryCursorContainerProps, -) => { - const props = { ...defaultProps, ...initialProps }; - const { children } = props; - - const getCursorPosition = () => { - const { cursorValue, defaultCursorValue, domain, cursorDimension } = props; - if (cursorValue) { - return cursorValue; - } - - if (typeof defaultCursorValue === "number") { - return { - x: (domain.x[0] + domain.x[1]) / 2, - y: (domain.y[0] + domain.y[1]) / 2, - [cursorDimension]: defaultCursorValue, - }; - } - - return defaultCursorValue; - }; - - const getCursorLabelOffset = () => { - const { cursorLabelOffset } = props; - - if (typeof cursorLabelOffset === "number") { - return { - x: cursorLabelOffset, - y: cursorLabelOffset, - }; - } - - return cursorLabelOffset; - }; - - const getPadding = () => { - if (props.padding === undefined) { - const child = props.children.find((c) => { - return isObject(c.props) && c.props.padding !== undefined; - }); - return Helpers.getPadding(child.props); - } - return Helpers.getPadding(props); - }; - - const getCursorElements = () => { - // eslint-disable-line max-statements - const { - scale, - cursorLabelComponent, - cursorLabel, - cursorComponent, - width, - height, - name, - horizontal, - theme, - } = props; - const cursorDimension = CursorHelpers.getDimension(props); - const cursorValue = getCursorPosition(); - const cursorLabelOffset = getCursorLabelOffset(); - - if (!cursorValue) { - return []; - } - - const newElements: React.ReactElement[] = []; - const padding = getPadding(); - const cursorCoordinates = { - x: horizontal ? scale.y(cursorValue.y) : scale.x(cursorValue.x), - y: horizontal ? scale.x(cursorValue.x) : scale.y(cursorValue.y), - }; - if (cursorLabel) { - let labelProps = defaults({ active: true }, cursorLabelComponent.props, { - x: cursorCoordinates.x + cursorLabelOffset.x, - y: cursorCoordinates.y + cursorLabelOffset.y, - datum: cursorValue, - active: true, - key: `${name}-cursor-label`, - }); - if (Helpers.isTooltip(cursorLabelComponent)) { - const tooltipTheme = (theme && theme.tooltip) || {}; - labelProps = defaults({}, labelProps, tooltipTheme); - } - newElements.push( - React.cloneElement( - cursorLabelComponent, - defaults({}, labelProps, { - text: Helpers.evaluateProp(cursorLabel, labelProps), - }), - ), - ); - } - - const cursorStyle = Object.assign( - { stroke: "black" }, - cursorComponent.props.style, - ); - if (cursorDimension === "x" || cursorDimension === undefined) { - newElements.push( - React.cloneElement(cursorComponent, { - key: `${name}-x-cursor`, - x1: cursorCoordinates.x, - x2: cursorCoordinates.x, - y1: padding.top, - y2: height - padding.bottom, - style: cursorStyle, - }), - ); - } - if (cursorDimension === "y" || cursorDimension === undefined) { - newElements.push( - React.cloneElement(cursorComponent, { - key: `${name}-y-cursor`, - x1: padding.left, - x2: width - padding.right, - y1: cursorCoordinates.y, - y2: cursorCoordinates.y, - style: cursorStyle, - }), - ); - } - return newElements; - }; - - return { - props, - children: [ - ...React.Children.toArray(children), - ...getCursorElements(), - ] as React.ReactElement[], - }; -}; - -export const VictoryCursorContainerFn = ( - initialProps: VictoryCursorContainerProps, -) => { - const { props, children } = useVictoryCursorContainer(initialProps); - return {children}; -}; - -VictoryCursorContainerFn.role = "container"; - -VictoryCursorContainerFn.defaultEvents = ( - initialProps: VictoryCursorContainerProps, -) => { - const props = { ...defaultProps, ...initialProps }; - const createEventHandler = - (handler: Handler, disabled?: boolean): Handler => - // eslint-disable-next-line max-params - (event, targetProps, eventKey, context) => - disabled || props.disable - ? {} - : handler(event, { ...props, ...targetProps }, eventKey, context); - - return [ - { - target: "parent", - eventHandlers: { - onMouseLeave: createEventHandler(CursorHelpers.onMouseLeave), - onMouseMove: createEventHandler(CursorHelpers.onMouseMove), - onTouchMove: createEventHandler(CursorHelpers.onMouseMove), - }, - }, - ]; -}; diff --git a/packages/victory-cursor-container/src/victory-cursor-container.tsx b/packages/victory-cursor-container/src/victory-cursor-container.tsx index f0501059a..84a501dea 100644 --- a/packages/victory-cursor-container/src/victory-cursor-container.tsx +++ b/packages/victory-cursor-container/src/victory-cursor-container.tsx @@ -1,6 +1,5 @@ import React from "react"; import { - VictoryContainer, Helpers, VictoryContainerProps, CoordinatesPropType, @@ -8,10 +7,18 @@ import { ValueOrAccessor, VictoryLabel, LineSegment, + VictoryContainerFn, } from "victory-core"; import { defaults, isObject } from "lodash"; import { CursorHelpers } from "./cursor-helpers"; +type Handler = ( + event: any, + targetProps: any, + eventKey?: any, + context?: any, +) => void; + export type CursorCoordinatesPropType = CoordinatesPropType | number; export interface VictoryCursorContainerProps extends VictoryContainerProps { @@ -28,185 +35,179 @@ export interface VictoryCursorContainerProps extends VictoryContainerProps { ) => void; } -type ComponentClass = { new (props: TProps): React.Component }; - -export function cursorContainerMixin< - TBase extends ComponentClass, - TProps extends VictoryCursorContainerProps, ->(Base: TBase) { - // @ts-expect-error "TS2545: A mixin class must have a constructor with a single rest parameter of type 'any[]'." - return class VictoryCursorContainer extends Base { - static displayName = "VictoryCursorContainer"; - static defaultProps = { - ...VictoryContainer.defaultProps, - cursorLabelComponent: , - cursorLabelOffset: { - x: 5, - y: -10, - }, - cursorComponent: , - }; - static defaultEvents = (props) => { - return [ - { - target: "parent", - eventHandlers: { - onMouseLeave: (evt, targetProps) => { - return props.disable - ? {} - : CursorHelpers.onMouseLeave(evt, targetProps); - }, - onTouchCancel: () => { - return []; - }, - onMouseMove: (evt, targetProps) => { - return props.disable - ? {} - : CursorHelpers.onMouseMove(evt, targetProps); - }, - onTouchMove: (evt, targetProps) => { - return props.disable - ? {} - : CursorHelpers.onMouseMove(evt, targetProps); - }, - }, - }, - ]; - }; - - getCursorPosition(props) { - const { cursorValue, defaultCursorValue, domain, cursorDimension } = - props; - if (cursorValue) { - return cursorValue; - } - - if (typeof defaultCursorValue === "number") { - return { - x: (domain.x[0] + domain.x[1]) / 2, - y: (domain.y[0] + domain.y[1]) / 2, - [cursorDimension]: defaultCursorValue, - }; - } +const defaultProps = { + cursorLabelComponent: , + cursorLabelOffset: { + x: 5, + y: -10, + }, + cursorComponent: , +}; + +export const useVictoryCursorContainer = ( + initialProps: VictoryCursorContainerProps, +) => { + const props = { ...defaultProps, ...initialProps }; + const { children } = props; + + const getCursorPosition = () => { + const { cursorValue, defaultCursorValue, domain, cursorDimension } = props; + if (cursorValue) { + return cursorValue; + } - return defaultCursorValue; + if (typeof defaultCursorValue === "number") { + return { + x: (domain.x[0] + domain.x[1]) / 2, + y: (domain.y[0] + domain.y[1]) / 2, + [cursorDimension]: defaultCursorValue, + }; } - getCursorLabelOffset(props) { - const { cursorLabelOffset } = props; + return defaultCursorValue; + }; - if (typeof cursorLabelOffset === "number") { - return { - x: cursorLabelOffset, - y: cursorLabelOffset, - }; - } + const getCursorLabelOffset = () => { + const { cursorLabelOffset } = props; - return cursorLabelOffset; + if (typeof cursorLabelOffset === "number") { + return { + x: cursorLabelOffset, + y: cursorLabelOffset, + }; } - getPadding(props) { - if (props.padding === undefined) { - const child = props.children.find((c) => { - return isObject(c.props) && c.props.padding !== undefined; - }); - return Helpers.getPadding(child.props); - } - return Helpers.getPadding(props); - } + return cursorLabelOffset; + }; - getCursorElements(props) { - // eslint-disable-line max-statements - const { - scale, - cursorLabelComponent, - cursorLabel, - cursorComponent, - width, - height, - name, - horizontal, - theme, - } = props; - const cursorDimension = CursorHelpers.getDimension(props); - const cursorValue = this.getCursorPosition(props); - const cursorLabelOffset = this.getCursorLabelOffset(props); - - if (!cursorValue) { - return []; - } + const getPadding = () => { + if (props.padding === undefined) { + const child = props.children.find((c) => { + return isObject(c.props) && c.props.padding !== undefined; + }); + return Helpers.getPadding(child.props); + } + return Helpers.getPadding(props); + }; - const newElements: React.ReactElement[] = []; - const padding = this.getPadding(props); - const cursorCoordinates = { - x: horizontal ? scale.y(cursorValue.y) : scale.x(cursorValue.x), - y: horizontal ? scale.x(cursorValue.x) : scale.y(cursorValue.y), - }; - if (cursorLabel) { - let labelProps = defaults( - { active: true }, - cursorLabelComponent.props, - { - x: cursorCoordinates.x + cursorLabelOffset.x, - y: cursorCoordinates.y + cursorLabelOffset.y, - datum: cursorValue, - active: true, - key: `${name}-cursor-label`, - }, - ); - if (Helpers.isTooltip(cursorLabelComponent)) { - const tooltipTheme = (theme && theme.tooltip) || {}; - labelProps = defaults({}, labelProps, tooltipTheme); - } - newElements.push( - React.cloneElement( - cursorLabelComponent, - defaults({}, labelProps, { - text: Helpers.evaluateProp(cursorLabel, labelProps), - }), - ), - ); - } + const getCursorElements = () => { + // eslint-disable-line max-statements + const { + scale, + cursorLabelComponent, + cursorLabel, + cursorComponent, + width, + height, + name, + horizontal, + theme, + } = props; + const cursorDimension = CursorHelpers.getDimension(props); + const cursorValue = getCursorPosition(); + const cursorLabelOffset = getCursorLabelOffset(); + + if (!cursorValue) { + return []; + } - const cursorStyle = Object.assign( - { stroke: "black" }, - cursorComponent.props.style, - ); - if (cursorDimension === "x" || cursorDimension === undefined) { - newElements.push( - React.cloneElement(cursorComponent, { - key: `${name}-x-cursor`, - x1: cursorCoordinates.x, - x2: cursorCoordinates.x, - y1: padding.top, - y2: height - padding.bottom, - style: cursorStyle, - }), - ); + const newElements: React.ReactElement[] = []; + const padding = getPadding(); + const cursorCoordinates = { + x: horizontal ? scale.y(cursorValue.y) : scale.x(cursorValue.x), + y: horizontal ? scale.x(cursorValue.x) : scale.y(cursorValue.y), + }; + if (cursorLabel) { + let labelProps = defaults({ active: true }, cursorLabelComponent.props, { + x: cursorCoordinates.x + cursorLabelOffset.x, + y: cursorCoordinates.y + cursorLabelOffset.y, + datum: cursorValue, + active: true, + key: `${name}-cursor-label`, + }); + if (Helpers.isTooltip(cursorLabelComponent)) { + const tooltipTheme = (theme && theme.tooltip) || {}; + labelProps = defaults({}, labelProps, tooltipTheme); } - if (cursorDimension === "y" || cursorDimension === undefined) { - newElements.push( - React.cloneElement(cursorComponent, { - key: `${name}-y-cursor`, - x1: padding.left, - x2: width - padding.right, - y1: cursorCoordinates.y, - y2: cursorCoordinates.y, - style: cursorStyle, + newElements.push( + React.cloneElement( + cursorLabelComponent, + defaults({}, labelProps, { + text: Helpers.evaluateProp(cursorLabel, labelProps), }), - ); - } - return newElements; + ), + ); } - // Overrides method in VictoryContainer - getChildren(props) { - return [ - ...React.Children.toArray(props.children), - ...this.getCursorElements(props), - ]; + const cursorStyle = Object.assign( + { stroke: "black" }, + cursorComponent.props.style, + ); + if (cursorDimension === "x" || cursorDimension === undefined) { + newElements.push( + React.cloneElement(cursorComponent, { + key: `${name}-x-cursor`, + x1: cursorCoordinates.x, + x2: cursorCoordinates.x, + y1: padding.top, + y2: height - padding.bottom, + style: cursorStyle, + }), + ); + } + if (cursorDimension === "y" || cursorDimension === undefined) { + newElements.push( + React.cloneElement(cursorComponent, { + key: `${name}-y-cursor`, + x1: padding.left, + x2: width - padding.right, + y1: cursorCoordinates.y, + y2: cursorCoordinates.y, + style: cursorStyle, + }), + ); } + return newElements; }; -} -export const VictoryCursorContainer = cursorContainerMixin(VictoryContainer); + return { + props, + children: [ + ...React.Children.toArray(children), + ...getCursorElements(), + ] as React.ReactElement[], + }; +}; + +export const VictoryCursorContainer = ( + initialProps: VictoryCursorContainerProps, +) => { + const { props, children } = useVictoryCursorContainer(initialProps); + return {children}; +}; + +VictoryCursorContainer.role = "container"; + +VictoryCursorContainer.defaultEvents = ( + initialProps: VictoryCursorContainerProps, +) => { + const props = { ...defaultProps, ...initialProps }; + const createEventHandler = + (handler: Handler, disabled?: boolean): Handler => + // eslint-disable-next-line max-params + (event, targetProps, eventKey, context) => + disabled || props.disable + ? {} + : handler(event, { ...props, ...targetProps }, eventKey, context); + + return [ + { + target: "parent", + eventHandlers: { + onMouseLeave: createEventHandler(CursorHelpers.onMouseLeave), + onMouseMove: createEventHandler(CursorHelpers.onMouseMove), + onTouchMove: createEventHandler(CursorHelpers.onMouseMove), + }, + }, + ]; +}; diff --git a/packages/victory-selection-container/src/index.ts b/packages/victory-selection-container/src/index.ts index a40972ae1..b779c7988 100644 --- a/packages/victory-selection-container/src/index.ts +++ b/packages/victory-selection-container/src/index.ts @@ -1,6 +1,2 @@ export * from "./victory-selection-container"; -export { - VictorySelectionContainerFn, - useVictorySelectionContainer, -} from "./victory-selection-container-fn"; export * from "./selection-helpers"; diff --git a/packages/victory-selection-container/src/victory-selection-container-fn.tsx b/packages/victory-selection-container/src/victory-selection-container-fn.tsx deleted file mode 100644 index b2698b776..000000000 --- a/packages/victory-selection-container/src/victory-selection-container-fn.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import React from "react"; -import { - Datum, - Rect, - VictoryContainerFn, - VictoryContainerProps, -} from "victory-core"; -import { SelectionHelpers } from "./selection-helpers"; - -type Handler = ( - event: any, - targetProps: any, - eventKey?: any, - context?: any, -) => void; - -export interface VictorySelectionContainerProps extends VictoryContainerProps { - activateSelectedData?: boolean; - allowSelection?: boolean; - disable?: boolean; - onSelection?: ( - points: { - childName?: string | string[]; - eventKey?: string | number; - data?: Datum[]; - }[], - bounds: { - x: number | Date; - y: number | Date; - }[], - props: VictorySelectionContainerProps, - ) => void; - horizontal?: boolean; - onSelectionCleared?: (props: VictorySelectionContainerProps) => void; - selectionBlacklist?: string[]; - selectionComponent?: React.ReactElement; - selectionDimension?: "x" | "y"; - selectionStyle?: React.CSSProperties; -} - -const defaultProps = { - activateSelectedData: true, - allowSelection: true, - selectionComponent: , - selectionStyle: { - stroke: "transparent", - fill: "black", - fillOpacity: 0.1, - }, -}; - -export const useVictorySelectionContainer = ( - initialProps: VictorySelectionContainerProps, -) => { - const props = { ...defaultProps, ...initialProps }; - - const { x1, x2, y1, y2, selectionStyle, selectionComponent, children, name } = - props; - const width = Math.abs(x2 - x1) || 1; - const height = Math.abs(y2 - y1) || 1; - const x = Math.min(x1, x2); - const y = Math.min(y1, y2); - - const shouldRenderRect = y1 && y2 && x1 && x2; - - return { - props, - children: [ - children, - shouldRenderRect && - React.cloneElement(selectionComponent, { - key: `${name}-selection`, - x, - y, - width, - height, - style: selectionStyle, - }), - ], - }; -}; - -export const VictorySelectionContainerFn = ( - initialProps: VictorySelectionContainerProps, -) => { - const { props, children } = useVictorySelectionContainer(initialProps); - return {children}; -}; - -VictorySelectionContainerFn.role = "container"; - -VictorySelectionContainerFn.defaultEvents = ( - initialProps: VictorySelectionContainerProps, -) => { - const props = { ...defaultProps, ...initialProps }; - const createEventHandler = - (handler: Handler, disabled?: boolean): Handler => - // eslint-disable-next-line max-params - (event, targetProps, eventKey, context) => - disabled || props.disable - ? {} - : handler(event, { ...props, ...targetProps }, eventKey, context); - - return [ - { - target: "parent", - eventHandlers: { - onMouseDown: createEventHandler(SelectionHelpers.onMouseDown), - onTouchStart: createEventHandler(SelectionHelpers.onMouseDown), - onMouseMove: createEventHandler(SelectionHelpers.onMouseMove), - onTouchMove: createEventHandler(SelectionHelpers.onMouseMove), - onMouseUp: createEventHandler(SelectionHelpers.onMouseUp), - onTouchEnd: createEventHandler(SelectionHelpers.onMouseUp), - }, - }, - ]; -}; diff --git a/packages/victory-selection-container/src/victory-selection-container.tsx b/packages/victory-selection-container/src/victory-selection-container.tsx index 1609aa727..dea1abdd7 100644 --- a/packages/victory-selection-container/src/victory-selection-container.tsx +++ b/packages/victory-selection-container/src/victory-selection-container.tsx @@ -2,11 +2,18 @@ import React from "react"; import { Datum, Rect, - VictoryContainer, + VictoryContainerFn, VictoryContainerProps, } from "victory-core"; import { SelectionHelpers } from "./selection-helpers"; +type Handler = ( + event: any, + targetProps: any, + eventKey?: any, + context?: any, +) => void; + export interface VictorySelectionContainerProps extends VictoryContainerProps { activateSelectedData?: boolean; allowSelection?: boolean; @@ -31,92 +38,80 @@ export interface VictorySelectionContainerProps extends VictoryContainerProps { selectionStyle?: React.CSSProperties; } -type ComponentClass = { new (props: TProps): React.Component }; +const defaultProps = { + activateSelectedData: true, + allowSelection: true, + selectionComponent: , + selectionStyle: { + stroke: "transparent", + fill: "black", + fillOpacity: 0.1, + }, +}; -export function selectionContainerMixin< - TBase extends ComponentClass, - TProps extends VictorySelectionContainerProps, ->(Base: TBase) { - // @ts-expect-error "TS2545: A mixin class must have a constructor with a single rest parameter of type 'any[]'." - return class VictorySelectionContainer extends Base { - static displayName = "VictorySelectionContainer"; - static defaultProps = { - ...VictoryContainer.defaultProps, - activateSelectedData: true, - allowSelection: true, - selectionComponent: , - selectionStyle: { - stroke: "transparent", - fill: "black", - fillOpacity: 0.1, - }, - }; +export const useVictorySelectionContainer = ( + initialProps: VictorySelectionContainerProps, +) => { + const props = { ...defaultProps, ...initialProps }; - static defaultEvents = (props: TProps) => { - return [ - { - target: "parent", - eventHandlers: { - onMouseDown: (evt, targetProps) => { - return props.disable - ? {} - : SelectionHelpers.onMouseDown(evt, targetProps); - }, - onTouchStart: (evt, targetProps) => { - return props.disable - ? {} - : SelectionHelpers.onMouseDown(evt, targetProps); - }, - onMouseMove: (evt, targetProps) => { - return props.disable - ? {} - : SelectionHelpers.onMouseMove(evt, targetProps); - }, - onTouchMove: (evt, targetProps) => { - return props.disable - ? {} - : SelectionHelpers.onMouseMove(evt, targetProps); - }, - onMouseUp: (evt, targetProps) => { - return props.disable - ? {} - : SelectionHelpers.onMouseUp(evt, targetProps); - }, - onTouchEnd: (evt, targetProps) => { - return props.disable - ? {} - : SelectionHelpers.onMouseUp(evt, targetProps); - }, - }, - }, - ]; - }; + const { x1, x2, y1, y2, selectionStyle, selectionComponent, children, name } = + props; + const width = Math.abs(x2 - x1) || 1; + const height = Math.abs(y2 - y1) || 1; + const x = Math.min(x1, x2); + const y = Math.min(y1, y2); - getRect(props) { - const { x1, x2, y1, y2, selectionStyle, selectionComponent, name } = - props; - const width = Math.abs(x2 - x1) || 1; - const height = Math.abs(y2 - y1) || 1; - const x = Math.min(x1, x2); - const y = Math.min(y1, y2); - return y2 && x2 && x1 && y1 - ? React.cloneElement(selectionComponent, { - key: `${name}-selection`, - x, - y, - width, - height, - style: selectionStyle, - }) - : null; - } + const shouldRenderRect = y1 && y2 && x1 && x2; - // Overrides method in VictoryContainer - getChildren(props: TProps) { - return [...React.Children.toArray(props.children), this.getRect(props)]; - } + return { + props, + children: [ + children, + shouldRenderRect && + React.cloneElement(selectionComponent, { + key: `${name}-selection`, + x, + y, + width, + height, + style: selectionStyle, + }), + ], }; -} +}; + +export const VictorySelectionContainer = ( + initialProps: VictorySelectionContainerProps, +) => { + const { props, children } = useVictorySelectionContainer(initialProps); + return {children}; +}; -export const VictorySelectionContainer = - selectionContainerMixin(VictoryContainer); +VictorySelectionContainer.role = "container"; + +VictorySelectionContainer.defaultEvents = ( + initialProps: VictorySelectionContainerProps, +) => { + const props = { ...defaultProps, ...initialProps }; + const createEventHandler = + (handler: Handler, disabled?: boolean): Handler => + // eslint-disable-next-line max-params + (event, targetProps, eventKey, context) => + disabled || props.disable + ? {} + : handler(event, { ...props, ...targetProps }, eventKey, context); + + return [ + { + target: "parent", + eventHandlers: { + onMouseDown: createEventHandler(SelectionHelpers.onMouseDown), + onTouchStart: createEventHandler(SelectionHelpers.onMouseDown), + onMouseMove: createEventHandler(SelectionHelpers.onMouseMove), + onTouchMove: createEventHandler(SelectionHelpers.onMouseMove), + onMouseUp: createEventHandler(SelectionHelpers.onMouseUp), + onTouchEnd: createEventHandler(SelectionHelpers.onMouseUp), + }, + }, + ]; +}; diff --git a/packages/victory-voronoi-container/src/index.ts b/packages/victory-voronoi-container/src/index.ts index 48d73f9d0..313e652a9 100644 --- a/packages/victory-voronoi-container/src/index.ts +++ b/packages/victory-voronoi-container/src/index.ts @@ -1,6 +1,2 @@ export * from "./victory-voronoi-container"; -export { - VictoryVoronoiContainerFn, - useVictoryVoronoiContainer, -} from "./victory-voronoi-container-fn"; export * from "./voronoi-helpers"; diff --git a/packages/victory-voronoi-container/src/victory-voronoi-container-fn.tsx b/packages/victory-voronoi-container/src/victory-voronoi-container-fn.tsx deleted file mode 100644 index 8690f68d5..000000000 --- a/packages/victory-voronoi-container/src/victory-voronoi-container-fn.tsx +++ /dev/null @@ -1,242 +0,0 @@ -/* eslint-disable react/no-multi-comp */ -import React from "react"; -import { defaults, pick } from "lodash"; -import { VictoryTooltip } from "victory-tooltip"; -import { - Helpers, - VictoryContainerProps, - PaddingProps, - VictoryContainerFn, -} from "victory-core"; -import { VoronoiHelpers } from "./voronoi-helpers"; - -type Handler = ( - event: any, - targetProps: any, - eventKey?: any, - context?: any, -) => void; - -export interface VictoryVoronoiContainerProps extends VictoryContainerProps { - activateData?: boolean; - activateLabels?: boolean; - disable?: boolean; - labels?: (point: any, index?: number, points?: any[]) => string; - labelComponent?: React.ReactElement; - mouseFollowTooltips?: boolean; - onActivated?: (points: any[], props: VictoryVoronoiContainerProps) => void; - onDeactivated?: (points: any[], props: VictoryVoronoiContainerProps) => void; - radius?: number; - voronoiBlacklist?: (string | RegExp)[]; - voronoiDimension?: "x" | "y"; - voronoiPadding?: PaddingProps; -} - -const defaultProps = { - activateData: true, - activateLabels: true, - labelComponent: , - voronoiPadding: 5, -}; - -const getPoint = (point) => { - const whitelist = ["_x", "_x1", "_x0", "_y", "_y1", "_y0"]; - return pick(point, whitelist); -}; - -export const useVictoryVoronoiContainer = ( - initialProps: VictoryVoronoiContainerProps, -) => { - const props = { ...defaultProps, ...initialProps }; - const { children } = props; - - const getDimension = () => { - const { horizontal, voronoiDimension } = props; - if (!horizontal || !voronoiDimension) { - return voronoiDimension; - } - return voronoiDimension === "x" ? "y" : "x"; - }; - - const getLabelPosition = (labelProps, points) => { - const { mousePosition, mouseFollowTooltips } = props; - const voronoiDimension = getDimension(); - const point = getPoint(points[0]); - const basePosition = Helpers.scalePoint(props, point); - - let center = mouseFollowTooltips ? mousePosition : undefined; - if (!voronoiDimension || points.length < 2) { - return { - ...basePosition, - center: defaults({}, labelProps.center, center), - }; - } - - const x = voronoiDimension === "y" ? mousePosition.x : basePosition.x; - const y = voronoiDimension === "x" ? mousePosition.y : basePosition.y; - center = mouseFollowTooltips ? mousePosition : { x, y }; - return { x, y, center: defaults({}, labelProps.center, center) }; - }; - - const getStyle = (points, type) => { - const { labels, labelComponent, theme } = props; - const componentProps = labelComponent.props || {}; - const themeStyles = - theme && theme.voronoi && theme.voronoi.style ? theme.voronoi.style : {}; - const componentStyleArray = - type === "flyout" ? componentProps.flyoutStyle : componentProps.style; - return points.reduce((memo, datum, index) => { - const labelProps = defaults({}, componentProps, { - datum, - active: true, - }); - const text = Helpers.isFunction(labels) ? labels(labelProps) : undefined; - const textArray = text !== undefined ? `${text}`.split("\n") : []; - const baseStyle = (datum.style && datum.style[type]) || {}; - const componentStyle = Array.isArray(componentStyleArray) - ? componentStyleArray[index] - : componentStyleArray; - const style = Helpers.evaluateStyle( - defaults({}, componentStyle, baseStyle, themeStyles[type]), - labelProps, - ); - const styleArray = textArray.length - ? textArray.map(() => style) - : [style]; - return memo.concat(styleArray); - }, []); - }; - - const getDefaultLabelProps = (points) => { - const { voronoiDimension, horizontal, mouseFollowTooltips } = props; - const point = getPoint(points[0]); - const multiPoint = voronoiDimension && points.length > 1; - const y = point._y1 !== undefined ? point._y1 : point._y; - const defaultHorizontalOrientation = y < 0 ? "left" : "right"; - const defaultOrientation = y < 0 ? "bottom" : "top"; - const labelOrientation = horizontal - ? defaultHorizontalOrientation - : defaultOrientation; - const orientation = mouseFollowTooltips ? undefined : labelOrientation; - return { - orientation, - pointerLength: multiPoint ? 0 : undefined, - constrainToVisibleArea: - multiPoint || mouseFollowTooltips ? true : undefined, - }; - }; - - const getLabelProps = (points) => { - const { labels, scale, labelComponent, theme, width, height } = props; - const componentProps = labelComponent.props || {}; - const text = points.reduce((memo, datum) => { - const labelProps = defaults({}, componentProps, { - datum, - active: true, - }); - const t = Helpers.isFunction(labels) ? labels(labelProps) : null; - if (t === null || t === undefined) { - return memo; - } - return memo.concat(`${t}`.split("\n")); - }, []); - - // remove properties from first point to make datum - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { childName, eventKey, style, continuous, ...datum } = points[0]; - const name = - props.name === childName ? childName : `${props.name}-${childName}`; - const labelProps = defaults( - { - key: `${name}-${eventKey}-voronoi-tooltip`, - id: `${name}-${eventKey}-voronoi-tooltip`, - active: true, - renderInPortal: false, - activePoints: points, - datum, - scale, - theme, - }, - componentProps, - { - text, - width, - height, - style: getStyle(points, "labels"), - flyoutStyle: getStyle(points, "flyout")[0], - }, - getDefaultLabelProps(points), - ); - const labelPosition = getLabelPosition(labelProps, points); - - return defaults({}, labelPosition, labelProps); - }; - - const getTooltip = () => { - const { labels, activePoints, labelComponent } = props; - if (!labels) { - return null; - } - if (Array.isArray(activePoints) && activePoints.length) { - const labelProps = getLabelProps(activePoints); - const { text } = labelProps; - const showLabel = Array.isArray(text) - ? text.filter(Boolean).length - : text; - return showLabel ? React.cloneElement(labelComponent, labelProps) : null; - } - return null; - }; - - return { - props, - children: [ - ...React.Children.toArray(children), - getTooltip(), - ] as React.ReactElement[], - }; -}; - -export const VictoryVoronoiContainerFn = ( - initialProps: VictoryVoronoiContainerProps, -) => { - const { props, children } = useVictoryVoronoiContainer(initialProps); - return {children}; -}; - -VictoryVoronoiContainerFn.role = "container"; - -VictoryVoronoiContainerFn.defaultEvents = ( - initialProps: VictoryVoronoiContainerProps, -) => { - const props = { ...defaultProps, ...initialProps }; - const createEventHandler = - (handler: Handler, disabled?: boolean): Handler => - // eslint-disable-next-line max-params - (event, targetProps, eventKey, context) => - disabled || props.disable - ? {} - : handler(event, { ...props, ...targetProps }, eventKey, context); - - return [ - { - target: "parent", - eventHandlers: { - onMouseLeave: createEventHandler(VoronoiHelpers.onMouseLeave), - onTouchCancel: createEventHandler(VoronoiHelpers.onMouseLeave), - onMouseMove: createEventHandler(VoronoiHelpers.onMouseMove), - onTouchMove: createEventHandler(VoronoiHelpers.onMouseMove), - }, - }, - { - target: "data", - eventHandlers: props.disable - ? {} - : { - onMouseOver: () => null, - onMouseOut: () => null, - onMouseMove: () => null, - }, - }, - ]; -}; diff --git a/packages/victory-voronoi-container/src/victory-voronoi-container.tsx b/packages/victory-voronoi-container/src/victory-voronoi-container.tsx index 034474e60..bb9faae8f 100644 --- a/packages/victory-voronoi-container/src/victory-voronoi-container.tsx +++ b/packages/victory-voronoi-container/src/victory-voronoi-container.tsx @@ -3,20 +3,25 @@ import React from "react"; import { defaults, pick } from "lodash"; import { VictoryTooltip } from "victory-tooltip"; import { - VictoryContainer, Helpers, VictoryContainerProps, PaddingProps, + VictoryContainerFn, } from "victory-core"; import { VoronoiHelpers } from "./voronoi-helpers"; -type ComponentClass = { new (props: TProps): React.Component }; +type Handler = ( + event: any, + targetProps: any, + eventKey?: any, + context?: any, +) => void; export interface VictoryVoronoiContainerProps extends VictoryContainerProps { activateData?: boolean; activateLabels?: boolean; disable?: boolean; - labels?: (point: any, index: number, points: any[]) => string; + labels?: (point: any, index?: number, points?: any[]) => string; labelComponent?: React.ReactElement; mouseFollowTooltips?: boolean; onActivated?: (points: any[], props: VictoryVoronoiContainerProps) => void; @@ -27,218 +32,211 @@ export interface VictoryVoronoiContainerProps extends VictoryContainerProps { voronoiPadding?: PaddingProps; } -export function voronoiContainerMixin< - TBase extends ComponentClass, - TProps extends VictoryVoronoiContainerProps, ->(Base: TBase) { - // @ts-expect-error "TS2545: A mixin class must have a constructor with a single rest parameter of type 'any[]'." - return class VictoryVoronoiContainer extends Base { - static displayName = "VictoryVoronoiContainer"; - - static defaultProps: VictoryVoronoiContainerProps = { - ...VictoryContainer.defaultProps, - activateData: true, - activateLabels: true, - labelComponent: , - voronoiPadding: 5, - }; - - static defaultEvents = (props: VictoryVoronoiContainerProps) => { - return [ - { - target: "parent", - eventHandlers: { - onMouseLeave: (evt, targetProps) => { - return props.disable - ? {} - : VoronoiHelpers.onMouseLeave(evt, targetProps); - }, - onTouchCancel: (evt, targetProps) => { - return props.disable - ? {} - : VoronoiHelpers.onMouseLeave(evt, targetProps); - }, - onMouseMove: (evt, targetProps) => { - return props.disable - ? {} - : VoronoiHelpers.onMouseMove(evt, targetProps); - }, - onTouchMove: (evt, targetProps) => { - return props.disable - ? {} - : VoronoiHelpers.onMouseMove(evt, targetProps); - }, - }, - }, - { - target: "data", - eventHandlers: props.disable - ? {} - : { - onMouseOver: () => null, - onMouseOut: () => null, - onMouseMove: () => null, - }, - }, - ]; - }; - - getDimension(props) { - const { horizontal, voronoiDimension } = props; - if (!horizontal || !voronoiDimension) { - return voronoiDimension; - } - return voronoiDimension === "x" ? "y" : "x"; - } - - getPoint(point) { - const whitelist = ["_x", "_x1", "_x0", "_y", "_y1", "_y0"]; - return pick(point, whitelist); - } - - getLabelPosition(props, labelProps, points) { - const { mousePosition, mouseFollowTooltips } = props; - const voronoiDimension = this.getDimension(props); - const point = this.getPoint(points[0]); - const basePosition = Helpers.scalePoint(props, point); - - let center = mouseFollowTooltips ? mousePosition : undefined; - if (!voronoiDimension || points.length < 2) { - return { - ...basePosition, - center: defaults({}, labelProps.center, center), - }; - } - - const x = voronoiDimension === "y" ? mousePosition.x : basePosition.x; - const y = voronoiDimension === "x" ? mousePosition.y : basePosition.y; - center = mouseFollowTooltips ? mousePosition : { x, y }; - return { x, y, center: defaults({}, labelProps.center, center) }; +const defaultProps = { + activateData: true, + activateLabels: true, + labelComponent: , + voronoiPadding: 5, +}; + +const getPoint = (point) => { + const whitelist = ["_x", "_x1", "_x0", "_y", "_y1", "_y0"]; + return pick(point, whitelist); +}; + +export const useVictoryVoronoiContainer = ( + initialProps: VictoryVoronoiContainerProps, +) => { + const props = { ...defaultProps, ...initialProps }; + const { children } = props; + + const getDimension = () => { + const { horizontal, voronoiDimension } = props; + if (!horizontal || !voronoiDimension) { + return voronoiDimension; } + return voronoiDimension === "x" ? "y" : "x"; + }; - getStyle(props, points, type) { - const { labels, labelComponent, theme } = props; - const componentProps = labelComponent.props || {}; - const themeStyles = - theme && theme.voronoi && theme.voronoi.style - ? theme.voronoi.style - : {}; - const componentStyleArray = - type === "flyout" ? componentProps.flyoutStyle : componentProps.style; - return points.reduce((memo, datum, index) => { - const labelProps = defaults({}, componentProps, { - datum, - active: true, - }); - const text = Helpers.isFunction(labels) - ? labels(labelProps) - : undefined; - const textArray = text !== undefined ? `${text}`.split("\n") : []; - const baseStyle = (datum.style && datum.style[type]) || {}; - const componentStyle = Array.isArray(componentStyleArray) - ? componentStyleArray[index] - : componentStyleArray; - const style = Helpers.evaluateStyle( - defaults({}, componentStyle, baseStyle, themeStyles[type]), - labelProps, - ); - const styleArray = textArray.length - ? textArray.map(() => style) - : [style]; - return memo.concat(styleArray); - }, []); - } + const getLabelPosition = (labelProps, points) => { + const { mousePosition, mouseFollowTooltips } = props; + const voronoiDimension = getDimension(); + const point = getPoint(points[0]); + const basePosition = Helpers.scalePoint(props, point); - getDefaultLabelProps(props, points) { - const { voronoiDimension, horizontal, mouseFollowTooltips } = props; - const point = this.getPoint(points[0]); - const multiPoint = voronoiDimension && points.length > 1; - const y = point._y1 !== undefined ? point._y1 : point._y; - const defaultHorizontalOrientation = y < 0 ? "left" : "right"; - const defaultOrientation = y < 0 ? "bottom" : "top"; - const labelOrientation = horizontal - ? defaultHorizontalOrientation - : defaultOrientation; - const orientation = mouseFollowTooltips ? undefined : labelOrientation; + let center = mouseFollowTooltips ? mousePosition : undefined; + if (!voronoiDimension || points.length < 2) { return { - orientation, - pointerLength: multiPoint ? 0 : undefined, - constrainToVisibleArea: - multiPoint || mouseFollowTooltips ? true : undefined, + ...basePosition, + center: defaults({}, labelProps.center, center), }; } - getLabelProps(props, points) { - const { labels, scale, labelComponent, theme, width, height } = props; - const componentProps = labelComponent.props || {}; - const text = points.reduce((memo, datum) => { - const labelProps = defaults({}, componentProps, { - datum, - active: true, - }); - const t = Helpers.isFunction(labels) ? labels(labelProps) : null; - if (t === null || t === undefined) { - return memo; - } - return memo.concat(`${t}`.split("\n")); - }, []); - - // remove properties from first point to make datum - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { childName, eventKey, style, continuous, ...datum } = points[0]; - const name = - props.name === childName ? childName : `${props.name}-${childName}`; - const labelProps = defaults( - { - key: `${name}-${eventKey}-voronoi-tooltip`, - id: `${name}-${eventKey}-voronoi-tooltip`, - active: true, - renderInPortal: false, - activePoints: points, - datum, - scale, - theme, - }, - componentProps, - { - text, - width, - height, - style: this.getStyle(props, points, "labels"), - flyoutStyle: this.getStyle(props, points, "flyout")[0], - }, - this.getDefaultLabelProps(props, points), + const x = voronoiDimension === "y" ? mousePosition.x : basePosition.x; + const y = voronoiDimension === "x" ? mousePosition.y : basePosition.y; + center = mouseFollowTooltips ? mousePosition : { x, y }; + return { x, y, center: defaults({}, labelProps.center, center) }; + }; + + const getStyle = (points, type) => { + const { labels, labelComponent, theme } = props; + const componentProps = labelComponent.props || {}; + const themeStyles = + theme && theme.voronoi && theme.voronoi.style ? theme.voronoi.style : {}; + const componentStyleArray = + type === "flyout" ? componentProps.flyoutStyle : componentProps.style; + return points.reduce((memo, datum, index) => { + const labelProps = defaults({}, componentProps, { + datum, + active: true, + }); + const text = Helpers.isFunction(labels) ? labels(labelProps) : undefined; + const textArray = text !== undefined ? `${text}`.split("\n") : []; + const baseStyle = (datum.style && datum.style[type]) || {}; + const componentStyle = Array.isArray(componentStyleArray) + ? componentStyleArray[index] + : componentStyleArray; + const style = Helpers.evaluateStyle( + defaults({}, componentStyle, baseStyle, themeStyles[type]), + labelProps, ); - const labelPosition = this.getLabelPosition(props, labelProps, points); - return defaults({}, labelPosition, labelProps); - } + const styleArray = textArray.length + ? textArray.map(() => style) + : [style]; + return memo.concat(styleArray); + }, []); + }; - getTooltip(props) { - const { labels, activePoints, labelComponent } = props; - if (!labels) { - return null; - } - if (Array.isArray(activePoints) && activePoints.length) { - const labelProps = this.getLabelProps(props, activePoints); - const { text } = labelProps; - const showLabel = Array.isArray(text) - ? text.filter(Boolean).length - : text; - return showLabel - ? React.cloneElement(labelComponent, labelProps) - : null; + const getDefaultLabelProps = (points) => { + const { voronoiDimension, horizontal, mouseFollowTooltips } = props; + const point = getPoint(points[0]); + const multiPoint = voronoiDimension && points.length > 1; + const y = point._y1 !== undefined ? point._y1 : point._y; + const defaultHorizontalOrientation = y < 0 ? "left" : "right"; + const defaultOrientation = y < 0 ? "bottom" : "top"; + const labelOrientation = horizontal + ? defaultHorizontalOrientation + : defaultOrientation; + const orientation = mouseFollowTooltips ? undefined : labelOrientation; + return { + orientation, + pointerLength: multiPoint ? 0 : undefined, + constrainToVisibleArea: + multiPoint || mouseFollowTooltips ? true : undefined, + }; + }; + + const getLabelProps = (points) => { + const { labels, scale, labelComponent, theme, width, height } = props; + const componentProps = labelComponent.props || {}; + const text = points.reduce((memo, datum) => { + const labelProps = defaults({}, componentProps, { + datum, + active: true, + }); + const t = Helpers.isFunction(labels) ? labels(labelProps) : null; + if (t === null || t === undefined) { + return memo; } + return memo.concat(`${t}`.split("\n")); + }, []); + + // remove properties from first point to make datum + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { childName, eventKey, style, continuous, ...datum } = points[0]; + const name = + props.name === childName ? childName : `${props.name}-${childName}`; + const labelProps = defaults( + { + key: `${name}-${eventKey}-voronoi-tooltip`, + id: `${name}-${eventKey}-voronoi-tooltip`, + active: true, + renderInPortal: false, + activePoints: points, + datum, + scale, + theme, + }, + componentProps, + { + text, + width, + height, + style: getStyle(points, "labels"), + flyoutStyle: getStyle(points, "flyout")[0], + }, + getDefaultLabelProps(points), + ); + const labelPosition = getLabelPosition(labelProps, points); + + return defaults({}, labelPosition, labelProps); + }; + + const getTooltip = () => { + const { labels, activePoints, labelComponent } = props; + if (!labels) { return null; } - - // Overrides method in VictoryContainer - getChildren(props: VictoryVoronoiContainerProps) { - return [ - ...React.Children.toArray(props.children), - this.getTooltip(props), - ]; + if (Array.isArray(activePoints) && activePoints.length) { + const labelProps = getLabelProps(activePoints); + const { text } = labelProps; + const showLabel = Array.isArray(text) + ? text.filter(Boolean).length + : text; + return showLabel ? React.cloneElement(labelComponent, labelProps) : null; } + return null; }; -} -export const VictoryVoronoiContainer = voronoiContainerMixin(VictoryContainer); + return { + props, + children: [ + ...React.Children.toArray(children), + getTooltip(), + ] as React.ReactElement[], + }; +}; + +export const VictoryVoronoiContainer = ( + initialProps: VictoryVoronoiContainerProps, +) => { + const { props, children } = useVictoryVoronoiContainer(initialProps); + return {children}; +}; + +VictoryVoronoiContainer.role = "container"; + +VictoryVoronoiContainer.defaultEvents = ( + initialProps: VictoryVoronoiContainerProps, +) => { + const props = { ...defaultProps, ...initialProps }; + const createEventHandler = + (handler: Handler, disabled?: boolean): Handler => + // eslint-disable-next-line max-params + (event, targetProps, eventKey, context) => + disabled || props.disable + ? {} + : handler(event, { ...props, ...targetProps }, eventKey, context); + + return [ + { + target: "parent", + eventHandlers: { + onMouseLeave: createEventHandler(VoronoiHelpers.onMouseLeave), + onTouchCancel: createEventHandler(VoronoiHelpers.onMouseLeave), + onMouseMove: createEventHandler(VoronoiHelpers.onMouseMove), + onTouchMove: createEventHandler(VoronoiHelpers.onMouseMove), + }, + }, + { + target: "data", + eventHandlers: props.disable + ? {} + : { + onMouseOver: () => null, + onMouseOut: () => null, + onMouseMove: () => null, + }, + }, + ]; +}; diff --git a/packages/victory-zoom-container/src/index.ts b/packages/victory-zoom-container/src/index.ts index eba9c925f..1a87c305d 100644 --- a/packages/victory-zoom-container/src/index.ts +++ b/packages/victory-zoom-container/src/index.ts @@ -1,6 +1,2 @@ export * from "./victory-zoom-container"; -export { - VictoryZoomContainerFn, - useVictoryZoomContainer, -} from "./victory-zoom-container-fn"; export * from "./zoom-helpers"; diff --git a/packages/victory-zoom-container/src/victory-zoom-container-fn.tsx b/packages/victory-zoom-container/src/victory-zoom-container-fn.tsx deleted file mode 100644 index 87a7e7326..000000000 --- a/packages/victory-zoom-container/src/victory-zoom-container-fn.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import React from "react"; -import { ZoomHelpers } from "./zoom-helpers"; -import { - VictoryClipContainer, - VictoryContainerProps, - DomainTuple, - VictoryContainerFn, - Data, -} from "victory-core"; -import { defaults } from "lodash"; - -const DEFAULT_DOWNSAMPLE = 150; - -export type ZoomDimensionType = "x" | "y"; - -export type ZoomDomain = { - x: DomainTuple; - y: DomainTuple; -}; - -type Handler = ( - event: any, - targetProps: any, - eventKey?: any, - context?: any, -) => void; - -export interface VictoryZoomContainerProps extends VictoryContainerProps { - allowPan?: boolean; - allowZoom?: boolean; - clipContainerComponent?: React.ReactElement; - disable?: boolean; - downsample?: number | boolean; - minimumZoom?: { x?: number; y?: number }; - onZoomDomainChange?: ( - domain: ZoomDomain, - props: VictoryZoomContainerProps, - ) => void; - zoomDimension?: ZoomDimensionType; - zoomDomain?: Partial; -} - -const defaultProps = { - clipContainerComponent: , - allowPan: true, - allowZoom: true, - zoomActive: false, -}; - -export const useVictoryZoomContainer = ( - initialProps: VictoryZoomContainerProps, -) => { - const props = { ...defaultProps, ...initialProps }; - const { - children, - currentDomain, - zoomActive, - allowZoom, - downsample, - scale, - clipContainerComponent, - polar, - origin, - horizontal, - } = props; - - const downsampleZoomData = (child: React.ReactElement, domain) => { - const getData = (childProps) => { - const { data, x, y } = childProps; - const defaultGetData = - child.type && typeof (child.type as any).getData === "function" - ? (child.type as any).getData - : () => undefined; - // skip costly data formatting if x and y accessors are not present - return Array.isArray(data) && !x && !y - ? data - : defaultGetData(childProps); - }; - - const data = getData(child.props); - - // return undefined if downsample is not run, then default() will replace with child.props.data - if (!downsample || !domain || !data) { - return undefined; - } - - const maxPoints = downsample === true ? DEFAULT_DOWNSAMPLE : downsample; - const dimension = props.zoomDimension || "x"; - - // important: assumes data is ordered by dimension - // get the start and end of the data that is in the current visible domain - let startIndex = data.findIndex( - (d) => d[dimension] >= domain[dimension][0], - ); - let endIndex = data.findIndex((d) => d[dimension] > domain[dimension][1]); - // pick one more point (if available) at each end so that VictoryLine, VictoryArea connect - if (startIndex !== 0) { - startIndex -= 1; - } - if (endIndex !== -1) { - endIndex += 1; - } - - const visibleData = data.slice(startIndex, endIndex); - - return Data.downsample(visibleData, maxPoints, startIndex); - }; - - const modifiedChildren = ( - React.Children.toArray(children) as React.ReactElement[] - ).map((child) => { - const role = (child as any).type && (child as any).type.role; - const isDataComponent = Data.isDataComponent(child); - const originalDomain = defaults({}, props.originalDomain, props.domain); - const zoomDomain = defaults({}, props.zoomDomain, props.domain); - const cachedZoomDomain = defaults({}, props.cachedZoomDomain, props.domain); - - let domain: ZoomDomain; - - if (!ZoomHelpers.checkDomainEquality(zoomDomain, cachedZoomDomain)) { - // if zoomDomain has been changed, use it - domain = zoomDomain; - } else if (allowZoom && !zoomActive) { - // if user has zoomed all the way out, use the child domain - domain = child.props.domain; - } else { - // default: use currentDomain, set by the event handlers - domain = defaults({}, currentDomain, originalDomain); - } - - let newDomain = props.polar - ? { - x: originalDomain.x, - y: [0, domain.y[1]], - } - : domain; - - if (newDomain && props.zoomDimension) { - // if zooming is restricted to a dimension, don't squash changes to zoomDomain in other dim - newDomain = { - ...zoomDomain, - [props.zoomDimension]: newDomain[props.zoomDimension], - }; - } - - // don't downsample stacked data - const childProps = - isDataComponent && role !== "stack" - ? { - domain: newDomain, - data: downsampleZoomData(child, newDomain), - } - : { domain: newDomain }; - - const newChild = React.cloneElement( - child, - defaults(childProps, child.props), - ); - - // Clip data components - if (Data.isDataComponent(newChild)) { - const rangeX = horizontal ? scale.y.range() : scale.x.range(); - const rangeY = horizontal ? scale.x.range() : scale.y.range(); - const plottableWidth = Math.abs(rangeX[0] - rangeX[1]); - const plottableHeight = Math.abs(rangeY[0] - rangeY[1]); - const radius = Math.max(...rangeY); - const groupComponent = React.cloneElement(clipContainerComponent, { - clipWidth: plottableWidth, - clipHeight: plottableHeight, - translateX: Math.min(...rangeX), - translateY: Math.min(...rangeY), - polar, - origin: polar ? origin : undefined, - radius: polar ? radius : undefined, - ...clipContainerComponent.props, - }); - - return React.cloneElement(newChild, { - groupComponent, - }); - } - - return newChild; - }); - - return { props, children: modifiedChildren }; -}; - -export const VictoryZoomContainerFn = ( - initialProps: VictoryZoomContainerProps, -) => { - const { props, children } = useVictoryZoomContainer(initialProps); - return {children}; -}; - -VictoryZoomContainerFn.role = "container"; - -VictoryZoomContainerFn.defaultEvents = ( - initialProps: VictoryZoomContainerProps, -) => { - const props = { ...defaultProps, ...initialProps }; - const createEventHandler = - (handler: Handler, disabled?: boolean): Handler => - // eslint-disable-next-line max-params - (event, targetProps, eventKey, context) => - disabled || props.disable - ? {} - : handler(event, { ...props, ...targetProps }, eventKey, context); - - return [ - { - target: "parent", - eventHandlers: { - onMouseDown: createEventHandler(ZoomHelpers.onMouseDown), - onTouchStart: createEventHandler(ZoomHelpers.onMouseDown), - onMouseUp: createEventHandler(ZoomHelpers.onMouseUp), - onTouchEnd: createEventHandler(ZoomHelpers.onMouseUp), - onMouseLeave: createEventHandler(ZoomHelpers.onMouseLeave), - onTouchCancel: createEventHandler(ZoomHelpers.onMouseLeave), - onMouseMove: createEventHandler(ZoomHelpers.onMouseMove), - onTouchMove: createEventHandler(ZoomHelpers.onMouseMove), - onWheel: createEventHandler(ZoomHelpers.onWheel, !props.allowZoom), - }, - }, - ]; -}; diff --git a/packages/victory-zoom-container/src/victory-zoom-container.tsx b/packages/victory-zoom-container/src/victory-zoom-container.tsx index 54fd02c49..2f52d074c 100644 --- a/packages/victory-zoom-container/src/victory-zoom-container.tsx +++ b/packages/victory-zoom-container/src/victory-zoom-container.tsx @@ -1,19 +1,30 @@ import React from "react"; -import { defaults } from "lodash"; import { ZoomHelpers } from "./zoom-helpers"; import { - Data, - DomainTuple, - Helpers, - VictoryContainer, VictoryClipContainer, VictoryContainerProps, + DomainTuple, + VictoryContainerFn, + Data, } from "victory-core"; +import { defaults } from "lodash"; const DEFAULT_DOWNSAMPLE = 150; export type ZoomDimensionType = "x" | "y"; +export type ZoomDomain = { + x: DomainTuple; + y: DomainTuple; +}; + +type Handler = ( + event: any, + targetProps: any, + eventKey?: any, + context?: any, +) => void; + export interface VictoryZoomContainerProps extends VictoryContainerProps { allowPan?: boolean; allowZoom?: boolean; @@ -22,92 +33,132 @@ export interface VictoryZoomContainerProps extends VictoryContainerProps { downsample?: number | boolean; minimumZoom?: { x?: number; y?: number }; onZoomDomainChange?: ( - domain: { x: DomainTuple; y: DomainTuple }, + domain: ZoomDomain, props: VictoryZoomContainerProps, ) => void; zoomDimension?: ZoomDimensionType; - zoomDomain?: { x?: DomainTuple; y?: DomainTuple }; + zoomDomain?: Partial; } -type ComponentClass = { new (props: TProps): React.Component }; - -export function zoomContainerMixin< - TBase extends ComponentClass, - TProps extends VictoryZoomContainerProps, ->(Base: TBase) { - // @ts-expect-error "TS2545: A mixin class must have a constructor with a single rest parameter of type 'any[]'." - return class VictoryZoomContainer extends Base { - static displayName = "VictoryZoomContainer"; - - static defaultProps = { - ...VictoryContainer.defaultProps, - clipContainerComponent: , - allowPan: true, - allowZoom: true, - zoomActive: false, +const defaultProps = { + clipContainerComponent: , + allowPan: true, + allowZoom: true, + zoomActive: false, +}; + +export const useVictoryZoomContainer = ( + initialProps: VictoryZoomContainerProps, +) => { + const props = { ...defaultProps, ...initialProps }; + const { + children, + currentDomain, + zoomActive, + allowZoom, + downsample, + scale, + clipContainerComponent, + polar, + origin, + horizontal, + } = props; + + const downsampleZoomData = (child: React.ReactElement, domain) => { + const getData = (childProps) => { + const { data, x, y } = childProps; + const defaultGetData = + child.type && typeof (child.type as any).getData === "function" + ? (child.type as any).getData + : () => undefined; + // skip costly data formatting if x and y accessors are not present + return Array.isArray(data) && !x && !y + ? data + : defaultGetData(childProps); }; - static defaultEvents = (props: TProps) => { - return [ - { - target: "parent", - eventHandlers: { - onMouseDown: (evt, targetProps) => { - return props.disable - ? {} - : ZoomHelpers.onMouseDown(evt, targetProps); - }, - onTouchStart: (evt, targetProps) => { - return props.disable - ? {} - : ZoomHelpers.onMouseDown(evt, targetProps); - }, - onMouseUp: (evt, targetProps) => { - return props.disable - ? {} - : ZoomHelpers.onMouseUp(evt, targetProps); - }, - onTouchEnd: (evt, targetProps) => { - return props.disable - ? {} - : ZoomHelpers.onMouseUp(evt, targetProps); - }, - onMouseLeave: (evt, targetProps) => { - return props.disable - ? {} - : ZoomHelpers.onMouseLeave(evt, targetProps); - }, - onTouchCancel: (evt, targetProps) => { - return props.disable - ? {} - : ZoomHelpers.onMouseLeave(evt, targetProps); - }, - // eslint-disable-next-line max-params - onMouseMove: (evt, targetProps, eventKey, ctx) => { - if (props.disable) { - return {}; - } - return ZoomHelpers.onMouseMove(evt, targetProps, eventKey, ctx); - }, - // eslint-disable-next-line max-params - onTouchMove: (evt, targetProps, eventKey, ctx) => { - if (props.disable) { - return {}; - } - evt.preventDefault(); - return ZoomHelpers.onMouseMove(evt, targetProps, eventKey, ctx); - }, - ...(props.disable || !props.allowZoom - ? {} - : { onWheel: ZoomHelpers.onWheel }), - }, - }, - ]; - }; + const data = getData(child.props); + + // return undefined if downsample is not run, then default() will replace with child.props.data + if (!downsample || !domain || !data) { + return undefined; + } + + const maxPoints = downsample === true ? DEFAULT_DOWNSAMPLE : downsample; + const dimension = props.zoomDimension || "x"; + + // important: assumes data is ordered by dimension + // get the start and end of the data that is in the current visible domain + let startIndex = data.findIndex( + (d) => d[dimension] >= domain[dimension][0], + ); + let endIndex = data.findIndex((d) => d[dimension] > domain[dimension][1]); + // pick one more point (if available) at each end so that VictoryLine, VictoryArea connect + if (startIndex !== 0) { + startIndex -= 1; + } + if (endIndex !== -1) { + endIndex += 1; + } + + const visibleData = data.slice(startIndex, endIndex); + + return Data.downsample(visibleData, maxPoints, startIndex); + }; + + const modifiedChildren = ( + React.Children.toArray(children) as React.ReactElement[] + ).map((child) => { + const role = (child as any).type && (child as any).type.role; + const isDataComponent = Data.isDataComponent(child); + const originalDomain = defaults({}, props.originalDomain, props.domain); + const zoomDomain = defaults({}, props.zoomDomain, props.domain); + const cachedZoomDomain = defaults({}, props.cachedZoomDomain, props.domain); + + let domain: ZoomDomain; + + if (!ZoomHelpers.checkDomainEquality(zoomDomain, cachedZoomDomain)) { + // if zoomDomain has been changed, use it + domain = zoomDomain; + } else if (allowZoom && !zoomActive) { + // if user has zoomed all the way out, use the child domain + domain = child.props.domain; + } else { + // default: use currentDomain, set by the event handlers + domain = defaults({}, currentDomain, originalDomain); + } + + let newDomain = props.polar + ? { + x: originalDomain.x, + y: [0, domain.y[1]], + } + : domain; - clipDataComponents(children: React.ReactElement[], props) { - const { scale, clipContainerComponent, polar, origin, horizontal } = - props; + if (newDomain && props.zoomDimension) { + // if zooming is restricted to a dimension, don't squash changes to zoomDomain in other dim + newDomain = { + ...zoomDomain, + [props.zoomDimension]: newDomain[props.zoomDimension], + }; + } + + // don't downsample stacked data + const childProps = + isDataComponent && role !== "stack" + ? { + domain: newDomain, + data: downsampleZoomData(child, newDomain), + } + : { domain: newDomain }; + + const newChild = React.cloneElement( + child, + defaults(childProps, child.props), + ); + + // Clip data components + if (Data.isDataComponent(newChild)) { const rangeX = horizontal ? scale.y.range() : scale.x.range(); const rangeY = horizontal ? scale.x.range() : scale.y.range(); const plottableWidth = Math.abs(rangeX[0] - rangeX[1]); @@ -123,124 +174,53 @@ export function zoomContainerMixin< radius: polar ? radius : undefined, ...clipContainerComponent.props, }); - return React.Children.toArray(children).map((child) => { - if (!Data.isDataComponent(child)) { - return child; - } - return React.cloneElement(child as React.ReactElement, { - groupComponent, - }); - }); - } - - modifyPolarDomain(domain, originalDomain) { - // Only zoom the radius of polar charts. Zooming angles is very confusing - return { - x: originalDomain.x, - y: [0, domain.y[1]], - }; - } - - downsampleZoomData(props, child, domain) { - const { downsample } = props; - - const getData = (childProps) => { - const { data, x, y } = childProps; - const defaultGetData = - child.type && Helpers.isFunction(child.type.getData) - ? child.type.getData - : () => undefined; - // skip costly data formatting if x and y accessors are not present - return Array.isArray(data) && !x && !y - ? data - : defaultGetData(childProps); - }; - - const data = getData(child.props); - - // return undefined if downsample is not run, then default() will replace with child.props.data - if (!downsample || !domain || !data) { - return undefined; - } - - const maxPoints = downsample === true ? DEFAULT_DOWNSAMPLE : downsample; - const dimension = props.zoomDimension || "x"; - - // important: assumes data is ordered by dimension - // get the start and end of the data that is in the current visible domain - let startIndex = data.findIndex( - (d) => d[dimension] >= domain[dimension][0], - ); - let endIndex = data.findIndex((d) => d[dimension] > domain[dimension][1]); - // pick one more point (if available) at each end so that VictoryLine, VictoryArea connect - if (startIndex !== 0) { - startIndex -= 1; - } - if (endIndex !== -1) { - endIndex += 1; - } - - const visibleData = data.slice(startIndex, endIndex); - - return Data.downsample(visibleData, maxPoints, startIndex); - } - - modifyChildren(props) { - const childComponents = React.Children.toArray( - props.children, - ) as React.ReactElement[]; - - return childComponents.map((child) => { - const role = child.type && (child.type as any).role; - const isDataComponent = Data.isDataComponent(child); - const { currentDomain, zoomActive, allowZoom } = props; - const originalDomain = defaults({}, props.originalDomain, props.domain); - const zoomDomain = defaults({}, props.zoomDomain, props.domain); - const cachedZoomDomain = defaults( - {}, - props.cachedZoomDomain, - props.domain, - ); - let domain; - if (!ZoomHelpers.checkDomainEquality(zoomDomain, cachedZoomDomain)) { - // if zoomDomain has been changed, use it - domain = zoomDomain; - } else if (allowZoom && !zoomActive) { - // if user has zoomed all the way out, use the child domain - domain = child.props.domain; - } else { - // default: use currentDomain, set by the event handlers - domain = defaults({}, currentDomain, originalDomain); - } - let newDomain = props.polar - ? this.modifyPolarDomain(domain, originalDomain) - : domain; - if (newDomain && props.zoomDimension) { - // if zooming is restricted to a dimension, don't squash changes to zoomDomain in other dim - newDomain = { - ...zoomDomain, - [props.zoomDimension]: newDomain[props.zoomDimension], - }; - } - // don't downsample stacked data - const newProps = - isDataComponent && role !== "stack" - ? { - domain: newDomain, - data: this.downsampleZoomData(props, child, newDomain), - } - : { domain: newDomain }; - return React.cloneElement(child, defaults(newProps, child.props)); + return React.cloneElement(newChild, { + groupComponent, }); } - // Overrides method in VictoryContainer - getChildren(props: TProps) { - const children = this.modifyChildren(props); - return this.clipDataComponents(children, props); - } - }; -} - -export const VictoryZoomContainer = zoomContainerMixin(VictoryContainer); + return newChild; + }); + + return { props, children: modifiedChildren }; +}; + +export const VictoryZoomContainer = ( + initialProps: VictoryZoomContainerProps, +) => { + const { props, children } = useVictoryZoomContainer(initialProps); + return {children}; +}; + +VictoryZoomContainer.role = "container"; + +VictoryZoomContainer.defaultEvents = ( + initialProps: VictoryZoomContainerProps, +) => { + const props = { ...defaultProps, ...initialProps }; + const createEventHandler = + (handler: Handler, disabled?: boolean): Handler => + // eslint-disable-next-line max-params + (event, targetProps, eventKey, context) => + disabled || props.disable + ? {} + : handler(event, { ...props, ...targetProps }, eventKey, context); + + return [ + { + target: "parent", + eventHandlers: { + onMouseDown: createEventHandler(ZoomHelpers.onMouseDown), + onTouchStart: createEventHandler(ZoomHelpers.onMouseDown), + onMouseUp: createEventHandler(ZoomHelpers.onMouseUp), + onTouchEnd: createEventHandler(ZoomHelpers.onMouseUp), + onMouseLeave: createEventHandler(ZoomHelpers.onMouseLeave), + onTouchCancel: createEventHandler(ZoomHelpers.onMouseLeave), + onMouseMove: createEventHandler(ZoomHelpers.onMouseMove), + onTouchMove: createEventHandler(ZoomHelpers.onMouseMove), + onWheel: createEventHandler(ZoomHelpers.onWheel, !props.allowZoom), + }, + }, + ]; +}; From 19cbc01b1a791ee23be863f3186c7a99f1539855 Mon Sep 17 00:00:00 2001 From: Kenan Date: Tue, 13 Feb 2024 16:04:11 +0000 Subject: [PATCH 10/36] reset demos --- demo/ts/components/create-container-demo.tsx | 4 ++-- demo/ts/components/selection-demo.tsx | 3 +-- .../victory-brush-container-demo.tsx | 3 +-- .../victory-cursor-container-demo.tsx | 3 +-- .../victory-voronoi-container-demo.tsx | 3 +-- .../victory-zoom-container-demo.tsx | 19 +++++-------------- 6 files changed, 11 insertions(+), 24 deletions(-) diff --git a/demo/ts/components/create-container-demo.tsx b/demo/ts/components/create-container-demo.tsx index 16f3e1d38..778504cf0 100644 --- a/demo/ts/components/create-container-demo.tsx +++ b/demo/ts/components/create-container-demo.tsx @@ -5,7 +5,7 @@ import { round } from "lodash"; import { VictoryChart } from "victory-chart"; import { VictoryStack } from "victory-stack"; import { VictoryGroup } from "victory-group"; -import { createContainerFn as createContainer } from "victory-create-container"; +import { createContainer } from "victory-create-container"; import { VictoryBar } from "victory-bar"; import { VictoryLine } from "victory-line"; import { VictoryScatter } from "victory-scatter"; @@ -273,7 +273,7 @@ class App extends React.Component { render() { return (
- + diff --git a/demo/ts/components/selection-demo.tsx b/demo/ts/components/selection-demo.tsx index 7c4b7da29..4c986e597 100644 --- a/demo/ts/components/selection-demo.tsx +++ b/demo/ts/components/selection-demo.tsx @@ -6,8 +6,7 @@ import { VictoryGroup } from "victory-group"; import { VictoryBar } from "victory-bar"; import { VictoryLine } from "victory-line"; import { VictoryScatter } from "victory-scatter"; -// import { VictorySelectionContainer } from "victory-selection-container"; -import { VictorySelectionContainerFn as VictorySelectionContainer } from "victory-selection-container"; +import { VictorySelectionContainer } from "victory-selection-container"; import { VictoryLegend } from "victory-legend"; import { VictoryTooltip } from "victory-tooltip"; diff --git a/demo/ts/components/victory-brush-container-demo.tsx b/demo/ts/components/victory-brush-container-demo.tsx index ea8ab4e98..5dfd49fd7 100644 --- a/demo/ts/components/victory-brush-container-demo.tsx +++ b/demo/ts/components/victory-brush-container-demo.tsx @@ -9,8 +9,7 @@ import { VictoryLine } from "victory-line"; import { VictoryScatter } from "victory-scatter"; import { VictoryLegend } from "victory-legend"; import { VictoryZoomContainer } from "victory-zoom-container"; -import { VictoryBrushContainerFn as VictoryBrushContainer } from "victory-brush-container"; -// import { VictoryBrushContainer } from "victory-brush-container"; +import { VictoryBrushContainer } from "victory-brush-container"; import { DomainTuple } from "victory-core"; interface VictoryBrushContainerDemoState { diff --git a/demo/ts/components/victory-cursor-container-demo.tsx b/demo/ts/components/victory-cursor-container-demo.tsx index 928bba28c..2ad25dd14 100644 --- a/demo/ts/components/victory-cursor-container-demo.tsx +++ b/demo/ts/components/victory-cursor-container-demo.tsx @@ -7,8 +7,7 @@ import { VictoryGroup } from "victory-group"; import { VictoryBar } from "victory-bar"; import { VictoryLine } from "victory-line"; import { VictoryScatter } from "victory-scatter"; -import { VictoryCursorContainerFn as VictoryCursorContainer } from "victory-cursor-container"; -// import { VictoryCursorContainer } from "victory-cursor-container"; +import { VictoryCursorContainer } from "victory-cursor-container"; import { VictoryTooltip } from "victory-tooltip"; import { VictoryLegend } from "victory-legend"; import { VictoryTheme, CoordinatesPropType } from "victory-core"; diff --git a/demo/ts/components/victory-voronoi-container-demo.tsx b/demo/ts/components/victory-voronoi-container-demo.tsx index 22ccdf6c9..eee4f9499 100644 --- a/demo/ts/components/victory-voronoi-container-demo.tsx +++ b/demo/ts/components/victory-voronoi-container-demo.tsx @@ -7,8 +7,7 @@ import { VictoryGroup } from "victory-group"; import { VictoryBar } from "victory-bar"; import { VictoryLine } from "victory-line"; import { VictoryScatter } from "victory-scatter"; -// import { VictoryVoronoiContainer } from "victory-voronoi-container"; -import { VictoryVoronoiContainerFn as VictoryVoronoiContainer } from "victory-voronoi-container"; +import { VictoryVoronoiContainer } from "victory-voronoi-container"; import { Flyout, VictoryTooltip } from "victory-tooltip"; import { VictoryLegend } from "victory-legend"; import { VictoryLabel, VictoryTheme } from "victory-core"; diff --git a/demo/ts/components/victory-zoom-container-demo.tsx b/demo/ts/components/victory-zoom-container-demo.tsx index bd9b147c9..2eab1a3af 100644 --- a/demo/ts/components/victory-zoom-container-demo.tsx +++ b/demo/ts/components/victory-zoom-container-demo.tsx @@ -9,8 +9,7 @@ import { VictoryArea } from "victory-area"; import { VictoryBar } from "victory-bar"; import { VictoryLine } from "victory-line"; import { VictoryScatter } from "victory-scatter"; -import { VictoryZoomContainerFn as VictoryZoomContainer } from "victory-zoom-container"; -// import { VictoryZoomContainer } from "victory-zoom-container"; +import { VictoryZoomContainer } from "victory-zoom-container"; import { VictoryTooltip } from "victory-tooltip"; import { VictoryLegend } from "victory-legend"; import { @@ -332,9 +331,7 @@ export default class VictoryZoomContainerDemo extends React.Component< { mutation: (props) => { return { - style: Object.assign({}, props.style, { - stroke: "orange", - }), + style: Object.assign({}, props.style, { stroke: "orange" }), }; }, }, @@ -420,9 +417,7 @@ export default class VictoryZoomContainerDemo extends React.Component< target: "data", mutation: (props) => { return { - style: Object.assign({}, props.style, { - fill: "gold", - }), + style: Object.assign({}, props.style, { fill: "gold" }), }; }, }, @@ -431,9 +426,7 @@ export default class VictoryZoomContainerDemo extends React.Component< target: "data", mutation: (props) => { return { - style: Object.assign({}, props.style, { - fill: "orange", - }), + style: Object.assign({}, props.style, { fill: "orange" }), }; }, }, @@ -442,9 +435,7 @@ export default class VictoryZoomContainerDemo extends React.Component< target: "data", mutation: (props) => { return { - style: Object.assign({}, props.style, { - fill: "red", - }), + style: Object.assign({}, props.style, { fill: "red" }), }; }, }, From 42b9225d80f301c96c06f0800f09d08d4b68e9f3 Mon Sep 17 00:00:00 2001 From: Kenan Date: Tue, 13 Feb 2024 16:05:31 +0000 Subject: [PATCH 11/36] remove class victory-container --- .../victory-container-fn.tsx | 183 ---------- .../victory-container/victory-container.tsx | 340 ++++++++---------- 2 files changed, 141 insertions(+), 382 deletions(-) delete mode 100644 packages/victory-core/src/victory-container/victory-container-fn.tsx diff --git a/packages/victory-core/src/victory-container/victory-container-fn.tsx b/packages/victory-core/src/victory-container/victory-container-fn.tsx deleted file mode 100644 index dbeb500a0..000000000 --- a/packages/victory-core/src/victory-container/victory-container-fn.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import React, { useRef } from "react"; -import { uniqueId } from "lodash"; -import { Portal } from "../victory-portal/portal"; -import { PortalContext } from "../victory-portal/portal-context"; -import * as UserProps from "../victory-util/user-props"; -import { OriginType } from "../victory-label/victory-label"; -import { D3Scale } from "../types/prop-types"; -import { VictoryThemeDefinition } from "../victory-theme/types"; -import { mergeRefs } from "../victory-util"; - -export interface VictoryContainerProps { - "aria-describedby"?: string; - "aria-labelledby"?: string; - children?: React.ReactElement | React.ReactElement[]; - className?: string; - containerId?: number | string; - containerRef?: React.Ref; - desc?: string; - events?: React.DOMAttributes; - height?: number; - name?: string; - origin?: OriginType; - polar?: boolean; - portalComponent?: React.ReactElement; - portalZIndex?: number; - preserveAspectRatio?: string; - responsive?: boolean; - role?: string; - scale?: { - x?: D3Scale; - y?: D3Scale; - }; - style?: React.CSSProperties; - tabIndex?: number; - theme?: VictoryThemeDefinition; - title?: string; - width?: number; - // Props defined by the Open UI Automation (OUIA) 1.0-RC spec - // See https://ouia.readthedocs.io/en/latest/README.html#ouia-component - ouiaId?: number | string; - ouiaSafe?: boolean; - ouiaType?: string; -} - -const defaultProps = { - className: "VictoryContainer", - portalComponent: , - portalZIndex: 99, - responsive: true, - role: "img", -}; - -export const VictoryContainerFn = (initialProps: VictoryContainerProps) => { - const props = { ...defaultProps, ...initialProps }; - const { - role, - title, - desc, - children, - className, - portalZIndex, - portalComponent, - width, - height, - style, - tabIndex, - responsive, - events, - ouiaId, - ouiaSafe, - ouiaType, - } = props; - - const containerRef = useRef(null); - - const portalRef = useRef(null); - - // Generated ID stored in ref because it needs to persist across renders - const generatedId = useRef(uniqueId("victory-container-")); - const containerId = props.containerId ?? generatedId; - - const getIdForElement = (elName: string) => `${containerId}-${elName}`; - - const userProps = UserProps.getSafeUserProps(props); - - const dimensions = responsive - ? { width: "100%", height: "100%" } - : { width, height }; - - const viewBox = responsive ? `0 0 ${width} ${height}` : undefined; - - const preserveAspectRatio = responsive - ? props.preserveAspectRatio - : undefined; - - const ariaLabelledBy = - [title && getIdForElement("title"), props["aria-labelledby"]] - .filter(Boolean) - .join(" ") || undefined; - - const ariaDescribedBy = - [desc && getIdForElement("desc"), props["aria-describedby"]] - .filter(Boolean) - .join(" ") || undefined; - - const handleWheel = (e: WheelEvent) => e.preventDefault(); - - React.useEffect(() => { - // TODO check that this works - if (!props.events?.onWheel) return; - - const container = containerRef?.current; - container?.addEventListener("wheel", handleWheel); - - return () => { - container?.removeEventListener("wheel", handleWheel); - }; - }, []); - - return ( - -
- - {title ? {title} : null} - {desc ? {desc} : null} - {children} - -
- {React.cloneElement(portalComponent, { - width, - height, - viewBox, - preserveAspectRatio, - style: { ...dimensions, overflow: "visible" }, - ref: portalRef, - })} -
-
-
- ); -}; - -VictoryContainerFn.role = "container"; diff --git a/packages/victory-core/src/victory-container/victory-container.tsx b/packages/victory-core/src/victory-container/victory-container.tsx index 15c18654b..dbeb500a0 100644 --- a/packages/victory-core/src/victory-container/victory-container.tsx +++ b/packages/victory-core/src/victory-container/victory-container.tsx @@ -1,13 +1,12 @@ -import React from "react"; -import { defaults, uniqueId, isObject } from "lodash"; +import React, { useRef } from "react"; +import { uniqueId } from "lodash"; 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"; import { OriginType } from "../victory-label/victory-label"; import { D3Scale } from "../types/prop-types"; import { VictoryThemeDefinition } from "../victory-theme/types"; +import { mergeRefs } from "../victory-util"; export interface VictoryContainerProps { "aria-describedby"?: string; @@ -21,9 +20,6 @@ export interface VictoryContainerProps { height?: number; name?: string; origin?: OriginType; - ouiaId?: number | string; - ouiaSafe?: boolean; - ouiaType?: string; polar?: boolean; portalComponent?: React.ReactElement; portalZIndex?: number; @@ -39,203 +35,149 @@ export interface VictoryContainerProps { theme?: VictoryThemeDefinition; title?: string; width?: number; + // Props defined by the Open UI Automation (OUIA) 1.0-RC spec + // See https://ouia.readthedocs.io/en/latest/README.html#ouia-component + ouiaId?: number | string; + ouiaSafe?: boolean; + ouiaType?: string; } -export { VictoryContainerFn } from "./victory-container-fn"; - -export class VictoryContainer< - TProps extends VictoryContainerProps, -> extends React.Component { - static displayName = "VictoryContainer"; - static role = "container"; - - static defaultProps = { - className: "VictoryContainer", - portalComponent: , - portalZIndex: 99, - responsive: true, - role: "img", - }; - - static contextType = TimerContext; - private containerId: VictoryContainerProps["containerId"]; - // @ts-expect-error Ref will be initialized on mount - private portalRef: Portal; - // @ts-expect-error Ref will be initialized on mount - private containerRef: HTMLElement; - private shouldHandleWheel: boolean; - - constructor(props: TProps) { - super(props); - this.containerId = - !isObject(props) || props.containerId === undefined - ? uniqueId("victory-container-") - : props.containerId; - - this.shouldHandleWheel = !!(props && props.events && props.events.onWheel); - } - savePortalRef = (portal) => { - this.portalRef = portal; - return portal; - }; - portalUpdate = (key, el) => this.portalRef.portalUpdate(key, el); - portalRegister = () => this.portalRef.portalRegister(); - portalDeregister = (key) => this.portalRef.portalDeregister(key); - - saveContainerRef = (container: HTMLElement) => { - if (Helpers.isFunction(this.props.containerRef)) { - this.props.containerRef(container); - } - this.containerRef = container; - return container; - }; - - handleWheel = (e) => e.preventDefault(); - - componentDidMount() { - if (this.shouldHandleWheel && this.containerRef) { - this.containerRef.addEventListener("wheel", this.handleWheel); - } - } - - componentWillUnmount() { - if (this.shouldHandleWheel && this.containerRef) { - this.containerRef.removeEventListener("wheel", this.handleWheel); - } - } - - getIdForElement(elementName) { - return `${this.containerId}-${elementName}`; - } - - // overridden in custom containers - getChildren(props) { - return props.children; - } - - // Get props defined by the Open UI Automation (OUIA) 1.0-RC spec - // See https://ouia.readthedocs.io/en/latest/README.html#ouia-component - getOUIAProps(props) { - const { ouiaId, ouiaSafe, ouiaType } = props; - return { - ...(ouiaId && { "data-ouia-component-id": ouiaId }), - ...(ouiaType && { "data-ouia-component-type": ouiaType }), - ...(ouiaSafe !== undefined && { "data-ouia-safe": ouiaSafe }), - }; - } - - renderContainer(props, svgProps, style) { - const { - title, - desc, - portalComponent, - className, - width, - height, - portalZIndex, - responsive, - } = props; - const children = this.getChildren(props); - const dimensions = responsive - ? { width: "100%", height: "100%" } - : { width, height }; - const divStyle = Object.assign( - { - pointerEvents: "none", - touchAction: "none", - position: "relative", - } as const, - dimensions, - ); - const portalDivStyle = Object.assign( - { zIndex: portalZIndex, position: "absolute", top: 0, left: 0 } as const, - dimensions, - ); - const svgStyle = Object.assign({ pointerEvents: "all" }, dimensions); - const portalSvgStyle = Object.assign({ overflow: "visible" }, dimensions); - const portalProps = { - width, - height, - viewBox: svgProps.viewBox, - preserveAspectRatio: svgProps.preserveAspectRatio, - style: portalSvgStyle, +const defaultProps = { + className: "VictoryContainer", + portalComponent: , + portalZIndex: 99, + responsive: true, + role: "img", +}; + +export const VictoryContainerFn = (initialProps: VictoryContainerProps) => { + const props = { ...defaultProps, ...initialProps }; + const { + role, + title, + desc, + children, + className, + portalZIndex, + portalComponent, + width, + height, + style, + tabIndex, + responsive, + events, + ouiaId, + ouiaSafe, + ouiaType, + } = props; + + const containerRef = useRef(null); + + const portalRef = useRef(null); + + // Generated ID stored in ref because it needs to persist across renders + const generatedId = useRef(uniqueId("victory-container-")); + const containerId = props.containerId ?? generatedId; + + const getIdForElement = (elName: string) => `${containerId}-${elName}`; + + const userProps = UserProps.getSafeUserProps(props); + + const dimensions = responsive + ? { width: "100%", height: "100%" } + : { width, height }; + + const viewBox = responsive ? `0 0 ${width} ${height}` : undefined; + + const preserveAspectRatio = responsive + ? props.preserveAspectRatio + : undefined; + + const ariaLabelledBy = + [title && getIdForElement("title"), props["aria-labelledby"]] + .filter(Boolean) + .join(" ") || undefined; + + const ariaDescribedBy = + [desc && getIdForElement("desc"), props["aria-describedby"]] + .filter(Boolean) + .join(" ") || undefined; + + const handleWheel = (e: WheelEvent) => e.preventDefault(); + + React.useEffect(() => { + // TODO check that this works + if (!props.events?.onWheel) return; + + const container = containerRef?.current; + container?.addEventListener("wheel", handleWheel); + + return () => { + container?.removeEventListener("wheel", handleWheel); }; - return ( - +
+ + {title ? {title} : null} + {desc ? {desc} : null} + {children} +
- - {title ? ( - {title} - ) : null} - {desc ? ( - {desc} - ) : null} - {children} - -
- {React.cloneElement(portalComponent, { - ...portalProps, - ref: this.savePortalRef, - })} -
+ {React.cloneElement(portalComponent, { + width, + height, + viewBox, + preserveAspectRatio, + style: { ...dimensions, overflow: "visible" }, + ref: portalRef, + })}
- - ); - } - - render() { - const { - width, - height, - responsive, - events, - title, - desc, - tabIndex, - 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 = Object.assign( - { - width, - height, - tabIndex, - role, - "aria-labelledby": - [ - title && this.getIdForElement("title"), - this.props["aria-labelledby"], - ] - .filter(Boolean) - .join(" ") || undefined, - "aria-describedby": - [desc && this.getIdForElement("desc"), this.props["aria-describedby"]] - .filter(Boolean) - .join(" ") || undefined, - viewBox: responsive ? `0 0 ${width} ${height}` : undefined, - preserveAspectRatio: responsive ? preserveAspectRatio : undefined, - ...userProps, - }, - events, - ); - return this.renderContainer(this.props, svgProps, style); - } -} +
+
+ ); +}; + +VictoryContainerFn.role = "container"; From cd69e6b2587e028ba88c543af79afd15e15a7a0e Mon Sep 17 00:00:00 2001 From: Kenan Date: Tue, 13 Feb 2024 16:11:12 +0000 Subject: [PATCH 12/36] fix victory-container name --- .../victory-core/src/victory-container/victory-container.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/victory-core/src/victory-container/victory-container.tsx b/packages/victory-core/src/victory-container/victory-container.tsx index dbeb500a0..cf7d6b331 100644 --- a/packages/victory-core/src/victory-container/victory-container.tsx +++ b/packages/victory-core/src/victory-container/victory-container.tsx @@ -50,7 +50,7 @@ const defaultProps = { role: "img", }; -export const VictoryContainerFn = (initialProps: VictoryContainerProps) => { +export const VictoryContainer = (initialProps: VictoryContainerProps) => { const props = { ...defaultProps, ...initialProps }; const { role, @@ -180,4 +180,4 @@ export const VictoryContainerFn = (initialProps: VictoryContainerProps) => { ); }; -VictoryContainerFn.role = "container"; +VictoryContainer.role = "container"; From ec330d0322806a4bd7b3c504b5fcee6083bb7170 Mon Sep 17 00:00:00 2001 From: Kenan Date: Tue, 13 Feb 2024 16:45:54 +0000 Subject: [PATCH 13/36] clean up --- .../src/victory-brush-container.tsx | 17 +++++++---------- packages/victory-core/src/types/prop-types.ts | 7 +++++++ .../src/create-container.tsx | 4 ++-- .../src/victory-cursor-container.tsx | 14 ++++---------- .../src/components/victory-brush-container.tsx | 15 +++------------ .../src/components/victory-brush-line.tsx | 15 +++------------ .../src/components/victory-container.tsx | 15 +++------------ .../src/components/victory-cursor-container.tsx | 15 +++------------ .../components/victory-selection-container.tsx | 15 +++------------ .../components/victory-voronoi-container.tsx | 15 +++------------ .../src/components/victory-zoom-container.tsx | 15 +++------------ .../src/victory-selection-container.tsx | 14 ++++---------- .../src/victory-voronoi-container.tsx | 14 ++++---------- .../src/victory-zoom-container.tsx | 14 ++++---------- 14 files changed, 53 insertions(+), 136 deletions(-) diff --git a/packages/victory-brush-container/src/victory-brush-container.tsx b/packages/victory-brush-container/src/victory-brush-container.tsx index dc9be5ace..8efa882be 100644 --- a/packages/victory-brush-container/src/victory-brush-container.tsx +++ b/packages/victory-brush-container/src/victory-brush-container.tsx @@ -4,19 +4,13 @@ import { Rect, DomainTuple, VictoryContainerProps, - VictoryContainerFn, + VictoryContainer, + VictoryEventHandler, } from "victory-core"; import { BrushHelpers } from "./brush-helpers"; import { defaults } from "lodash"; import isEqual from "react-fast-compare"; -type Handler = ( - event: any, - targetProps: any, - eventKey?: any, - context?: any, -) => void; - export interface VictoryBrushContainerProps extends VictoryContainerProps { allowDrag?: boolean; allowDraw?: boolean; @@ -179,7 +173,7 @@ export const VictoryBrushContainer = ( initialProps: VictoryBrushContainerProps, ) => { const { props, children } = useVictoryBrushContainer(initialProps); - return {children}; + return {children}; }; VictoryBrushContainer.role = "container"; @@ -189,7 +183,10 @@ VictoryBrushContainer.defaultEvents = ( ) => { const props = { ...defaultProps, ...initialProps }; const createEventHandler = - (handler: Handler, isDisabled?: (targetProps: any) => boolean): Handler => + ( + handler: VictoryEventHandler, + isDisabled?: (targetProps: any) => boolean, + ): VictoryEventHandler => // eslint-disable-next-line max-params (event, targetProps, eventKey, context) => props.disable || isDisabled?.(targetProps) diff --git a/packages/victory-core/src/types/prop-types.ts b/packages/victory-core/src/types/prop-types.ts index ac3fe5dc0..c718754ef 100644 --- a/packages/victory-core/src/types/prop-types.ts +++ b/packages/victory-core/src/types/prop-types.ts @@ -194,3 +194,10 @@ export type CoordinatesPropType = { x: number; y: number; }; + +export type VictoryEventHandler = ( + event?: any, + targetProps?: any, + eventKey?: any, + context?: any, +) => void; diff --git a/packages/victory-create-container/src/create-container.tsx b/packages/victory-create-container/src/create-container.tsx index f105bbeb4..5e63eb837 100644 --- a/packages/victory-create-container/src/create-container.tsx +++ b/packages/victory-create-container/src/create-container.tsx @@ -7,7 +7,7 @@ import { useVictorySelectionContainer, } from "victory-selection-container"; import React from "react"; -import { VictoryContainerFn } from "victory-core"; +import { VictoryContainer } from "victory-core"; import { forOwn, groupBy, isEmpty, toPairs } from "lodash"; import { VictoryVoronoiContainer, @@ -140,7 +140,7 @@ export function createContainerFn( children: childrenA, }); - return {childrenB}; + return {childrenB}; } NewContainer.displayName = `Victory${containerAName}${containerBName}Container`; diff --git a/packages/victory-cursor-container/src/victory-cursor-container.tsx b/packages/victory-cursor-container/src/victory-cursor-container.tsx index 84a501dea..198c4046f 100644 --- a/packages/victory-cursor-container/src/victory-cursor-container.tsx +++ b/packages/victory-cursor-container/src/victory-cursor-container.tsx @@ -7,18 +7,12 @@ import { ValueOrAccessor, VictoryLabel, LineSegment, - VictoryContainerFn, + VictoryContainer, + VictoryEventHandler, } from "victory-core"; import { defaults, isObject } from "lodash"; import { CursorHelpers } from "./cursor-helpers"; -type Handler = ( - event: any, - targetProps: any, - eventKey?: any, - context?: any, -) => void; - export type CursorCoordinatesPropType = CoordinatesPropType | number; export interface VictoryCursorContainerProps extends VictoryContainerProps { @@ -183,7 +177,7 @@ export const VictoryCursorContainer = ( initialProps: VictoryCursorContainerProps, ) => { const { props, children } = useVictoryCursorContainer(initialProps); - return {children}; + return {children}; }; VictoryCursorContainer.role = "container"; @@ -193,7 +187,7 @@ VictoryCursorContainer.defaultEvents = ( ) => { const props = { ...defaultProps, ...initialProps }; const createEventHandler = - (handler: Handler, disabled?: boolean): Handler => + (handler: VictoryEventHandler, disabled?: boolean): VictoryEventHandler => // eslint-disable-next-line max-params (event, targetProps, eventKey, context) => disabled || props.disable diff --git a/packages/victory-native/src/components/victory-brush-container.tsx b/packages/victory-native/src/components/victory-brush-container.tsx index 371fbc8f7..1b90bee72 100644 --- a/packages/victory-native/src/components/victory-brush-container.tsx +++ b/packages/victory-native/src/components/victory-brush-container.tsx @@ -9,22 +9,13 @@ import { } from "victory-brush-container"; import { VictoryContainer } from "./victory-container"; import NativeHelpers from "../helpers/native-helpers"; +import { VictoryEventHandler } from "victory-core"; export interface VictoryBrushContainerNativeProps extends VictoryBrushContainerProps { disableContainerEvents?: boolean; - onTouchStart?: ( - evt?: any, - targetProps?: any, - eventKey?: any, - ctx?: any, - ) => void; - onTouchEnd?: ( - evt?: any, - targetProps?: any, - eventKey?: any, - ctx?: any, - ) => void; + onTouchStart?: VictoryEventHandler; + onTouchEnd?: VictoryEventHandler; } // ensure the selection component get native styles diff --git a/packages/victory-native/src/components/victory-brush-line.tsx b/packages/victory-native/src/components/victory-brush-line.tsx index 2ef7a8a92..9fe05a896 100644 --- a/packages/victory-native/src/components/victory-brush-line.tsx +++ b/packages/victory-native/src/components/victory-brush-line.tsx @@ -2,6 +2,7 @@ import React from "react"; import { PanResponder } from "react-native"; import { G, Rect } from "react-native-svg"; import { get } from "lodash"; +import { VictoryEventHandler } from "victory-core"; import { VictoryBrushLine as VictoryBrushLineBase, VictoryBrushLineProps, @@ -12,18 +13,8 @@ import NativeHelpers from "../helpers/native-helpers"; // ensure the selection c import { wrapCoreComponent } from "../helpers/wrap-core-component"; export interface VictoryNativeBrushLineProps extends VictoryBrushLineProps { - onTouchStart?: ( - evt?: any, - targetProps?: any, - eventKey?: any, - ctx?: any, - ) => void; - onTouchEnd?: ( - evt?: any, - targetProps?: any, - eventKey?: any, - ctx?: any, - ) => void; + onTouchStart?: VictoryEventHandler; + onTouchEnd?: VictoryEventHandler; } const RectWithStyle = ({ diff --git a/packages/victory-native/src/components/victory-container.tsx b/packages/victory-native/src/components/victory-container.tsx index 9b09a58ca..30ee81c9e 100644 --- a/packages/victory-native/src/components/victory-container.tsx +++ b/packages/victory-native/src/components/victory-container.tsx @@ -5,6 +5,7 @@ import { View, PanResponder } from "react-native"; import { VictoryContainer as VictoryContainerBase, VictoryContainerProps, + VictoryEventHandler, } from "victory-core/es"; import NativeHelpers from "../helpers/native-helpers"; import { Portal } from "./victory-portal/portal"; @@ -14,18 +15,8 @@ const no = () => false; export interface VictoryContainerNativeProps extends VictoryContainerProps { disableContainerEvents?: boolean; - onTouchStart?: ( - evt?: any, - targetProps?: any, - eventKey?: any, - ctx?: any, - ) => void; - onTouchEnd?: ( - evt?: any, - targetProps?: any, - eventKey?: any, - ctx?: any, - ) => void; + onTouchStart?: VictoryEventHandler; + onTouchEnd?: VictoryEventHandler; } export class VictoryContainer extends VictoryContainerBase { diff --git a/packages/victory-native/src/components/victory-cursor-container.tsx b/packages/victory-native/src/components/victory-cursor-container.tsx index 145893da5..9893d1631 100644 --- a/packages/victory-native/src/components/victory-cursor-container.tsx +++ b/packages/victory-native/src/components/victory-cursor-container.tsx @@ -1,5 +1,6 @@ import React from "react"; import { flow } from "lodash"; +import { VictoryEventHandler } from "victory-core"; import { VictoryCursorContainer as VictoryCursorContainerBase, CursorHelpers, @@ -13,18 +14,8 @@ import { LineSegment } from "./victory-primitives/line-segment"; export interface VictoryCursorContainerNativeProps extends VictoryCursorContainerProps { disableContainerEvents?: boolean; - onTouchStart?: ( - evt?: any, - targetProps?: any, - eventKey?: any, - ctx?: any, - ) => void; - onTouchEnd?: ( - evt?: any, - targetProps?: any, - eventKey?: any, - ctx?: any, - ) => void; + onTouchStart?: VictoryEventHandler; + onTouchEnd?: VictoryEventHandler; } function nativeCursorMixin< diff --git a/packages/victory-native/src/components/victory-selection-container.tsx b/packages/victory-native/src/components/victory-selection-container.tsx index 0577e2687..4b8c9fc4a 100644 --- a/packages/victory-native/src/components/victory-selection-container.tsx +++ b/packages/victory-native/src/components/victory-selection-container.tsx @@ -1,6 +1,7 @@ import React from "react"; import { flow } from "lodash"; import { Rect } from "react-native-svg"; +import { VictoryEventHandler } from "victory-core"; import { VictorySelectionContainer as VictorySelectionContainerBase, SelectionHelpers, @@ -13,18 +14,8 @@ import NativeHelpers from "../helpers/native-helpers"; export interface VictorySelectionContainerNativeProps extends VictorySelectionContainerProps { disableContainerEvents?: boolean; - onTouchStart?: ( - evt?: any, - targetProps?: any, - eventKey?: any, - ctx?: any, - ) => void; - onTouchEnd?: ( - evt?: any, - targetProps?: any, - eventKey?: any, - ctx?: any, - ) => void; + onTouchStart?: VictoryEventHandler; + onTouchEnd?: VictoryEventHandler; } // ensure the selection component get native styles diff --git a/packages/victory-native/src/components/victory-voronoi-container.tsx b/packages/victory-native/src/components/victory-voronoi-container.tsx index 1ddf79d30..8a693593d 100644 --- a/packages/victory-native/src/components/victory-voronoi-container.tsx +++ b/packages/victory-native/src/components/victory-voronoi-container.tsx @@ -1,6 +1,7 @@ /* eslint-disable react/no-multi-comp */ import React from "react"; import { flow } from "lodash"; +import { VictoryEventHandler } from "victory-core"; import { VictoryVoronoiContainer as VictoryVoronoiContainerBase, VictoryVoronoiContainerProps, @@ -13,18 +14,8 @@ import { VictoryTooltip } from "./victory-tooltip"; export interface VictoryVoronoiContainerNativeProps extends VictoryVoronoiContainerProps { disableContainerEvents?: boolean; - onTouchStart?: ( - evt?: any, - targetProps?: any, - eventKey?: any, - ctx?: any, - ) => void; - onTouchEnd?: ( - evt?: any, - targetProps?: any, - eventKey?: any, - ctx?: any, - ) => void; + onTouchStart?: VictoryEventHandler; + onTouchEnd?: VictoryEventHandler; } function nativeVoronoiMixin< diff --git a/packages/victory-native/src/components/victory-zoom-container.tsx b/packages/victory-native/src/components/victory-zoom-container.tsx index e13773da1..9b178a2ee 100644 --- a/packages/victory-native/src/components/victory-zoom-container.tsx +++ b/packages/victory-native/src/components/victory-zoom-container.tsx @@ -2,6 +2,7 @@ import React, { ComponentClass } from "react"; import { flow } from "lodash"; import { VictoryContainer } from "./victory-container"; import { VictoryClipContainer } from "./victory-clip-container"; +import { VictoryEventHandler } from "victory-core"; import { VictoryZoomContainer as VictoryZoomContainerBase, VictoryZoomContainerProps, @@ -12,18 +13,8 @@ import NativeZoomHelpers from "../helpers/native-zoom-helpers"; export interface VictoryZoomContainerNativeProps extends VictoryZoomContainerProps { disableContainerEvents?: boolean; - onTouchStart?: ( - evt?: any, - targetProps?: any, - eventKey?: any, - ctx?: any, - ) => void; - onTouchEnd?: ( - evt?: any, - targetProps?: any, - eventKey?: any, - ctx?: any, - ) => void; + onTouchStart?: VictoryEventHandler; + onTouchEnd?: VictoryEventHandler; } function nativeZoomMixin< diff --git a/packages/victory-selection-container/src/victory-selection-container.tsx b/packages/victory-selection-container/src/victory-selection-container.tsx index dea1abdd7..e4534b984 100644 --- a/packages/victory-selection-container/src/victory-selection-container.tsx +++ b/packages/victory-selection-container/src/victory-selection-container.tsx @@ -2,18 +2,12 @@ import React from "react"; import { Datum, Rect, - VictoryContainerFn, + VictoryContainer, VictoryContainerProps, + VictoryEventHandler, } from "victory-core"; import { SelectionHelpers } from "./selection-helpers"; -type Handler = ( - event: any, - targetProps: any, - eventKey?: any, - context?: any, -) => void; - export interface VictorySelectionContainerProps extends VictoryContainerProps { activateSelectedData?: boolean; allowSelection?: boolean; @@ -84,7 +78,7 @@ export const VictorySelectionContainer = ( initialProps: VictorySelectionContainerProps, ) => { const { props, children } = useVictorySelectionContainer(initialProps); - return {children}; + return {children}; }; VictorySelectionContainer.role = "container"; @@ -94,7 +88,7 @@ VictorySelectionContainer.defaultEvents = ( ) => { const props = { ...defaultProps, ...initialProps }; const createEventHandler = - (handler: Handler, disabled?: boolean): Handler => + (handler: VictoryEventHandler, disabled?: boolean): VictoryEventHandler => // eslint-disable-next-line max-params (event, targetProps, eventKey, context) => disabled || props.disable diff --git a/packages/victory-voronoi-container/src/victory-voronoi-container.tsx b/packages/victory-voronoi-container/src/victory-voronoi-container.tsx index bb9faae8f..5ed99f7d0 100644 --- a/packages/victory-voronoi-container/src/victory-voronoi-container.tsx +++ b/packages/victory-voronoi-container/src/victory-voronoi-container.tsx @@ -6,17 +6,11 @@ import { Helpers, VictoryContainerProps, PaddingProps, - VictoryContainerFn, + VictoryContainer, + VictoryEventHandler, } from "victory-core"; import { VoronoiHelpers } from "./voronoi-helpers"; -type Handler = ( - event: any, - targetProps: any, - eventKey?: any, - context?: any, -) => void; - export interface VictoryVoronoiContainerProps extends VictoryContainerProps { activateData?: boolean; activateLabels?: boolean; @@ -201,7 +195,7 @@ export const VictoryVoronoiContainer = ( initialProps: VictoryVoronoiContainerProps, ) => { const { props, children } = useVictoryVoronoiContainer(initialProps); - return {children}; + return {children}; }; VictoryVoronoiContainer.role = "container"; @@ -211,7 +205,7 @@ VictoryVoronoiContainer.defaultEvents = ( ) => { const props = { ...defaultProps, ...initialProps }; const createEventHandler = - (handler: Handler, disabled?: boolean): Handler => + (handler: VictoryEventHandler, disabled?: boolean): VictoryEventHandler => // eslint-disable-next-line max-params (event, targetProps, eventKey, context) => disabled || props.disable diff --git a/packages/victory-zoom-container/src/victory-zoom-container.tsx b/packages/victory-zoom-container/src/victory-zoom-container.tsx index 2f52d074c..57db103ac 100644 --- a/packages/victory-zoom-container/src/victory-zoom-container.tsx +++ b/packages/victory-zoom-container/src/victory-zoom-container.tsx @@ -4,8 +4,9 @@ import { VictoryClipContainer, VictoryContainerProps, DomainTuple, - VictoryContainerFn, + VictoryContainer, Data, + VictoryEventHandler, } from "victory-core"; import { defaults } from "lodash"; @@ -18,13 +19,6 @@ export type ZoomDomain = { y: DomainTuple; }; -type Handler = ( - event: any, - targetProps: any, - eventKey?: any, - context?: any, -) => void; - export interface VictoryZoomContainerProps extends VictoryContainerProps { allowPan?: boolean; allowZoom?: boolean; @@ -190,7 +184,7 @@ export const VictoryZoomContainer = ( initialProps: VictoryZoomContainerProps, ) => { const { props, children } = useVictoryZoomContainer(initialProps); - return {children}; + return {children}; }; VictoryZoomContainer.role = "container"; @@ -200,7 +194,7 @@ VictoryZoomContainer.defaultEvents = ( ) => { const props = { ...defaultProps, ...initialProps }; const createEventHandler = - (handler: Handler, disabled?: boolean): Handler => + (handler: VictoryEventHandler, disabled?: boolean): VictoryEventHandler => // eslint-disable-next-line max-params (event, targetProps, eventKey, context) => disabled || props.disable From 920f3737a7259630d3a48dceae7efc56f631a3b1 Mon Sep 17 00:00:00 2001 From: Kenan Date: Wed, 14 Feb 2024 11:05:30 +0000 Subject: [PATCH 14/36] type create-container function properly --- .../src/create-container.tsx | 49 ++++++++----------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/packages/victory-create-container/src/create-container.tsx b/packages/victory-create-container/src/create-container.tsx index 5e63eb837..d854cbb8f 100644 --- a/packages/victory-create-container/src/create-container.tsx +++ b/packages/victory-create-container/src/create-container.tsx @@ -22,13 +22,6 @@ import { useVictoryBrushContainer, } from "victory-brush-container"; -export type ContainerType = - | "brush" - | "cursor" - | "selection" - | "voronoi" - | "zoom"; - function ensureArray(thing: T): [] | T | T[] { if (!thing) { return []; @@ -80,16 +73,7 @@ const combineDefaultEvents = (defaultEvents: any[]) => { return events.filter(Boolean); }; -type Container = { - name: string; - component: React.FC; - hook: (props: any) => { - props: any; - children: React.ReactNode; - }; -}; - -const CONTAINERS: Record = { +const CONTAINERS = { zoom: { name: "Zoom", component: VictoryZoomContainer, @@ -117,11 +101,16 @@ const CONTAINERS: Record = { }, }; -// TODO: Type this function properly -export function createContainerFn( - containerA: ContainerType, - containerB: ContainerType, -) { +export type ContainerType = keyof typeof CONTAINERS; + +type ContainerProps = React.ComponentProps< + typeof CONTAINERS[T]["component"] +>; + +export function createContainer< + TContainerAType extends ContainerType, + TContainerBType extends ContainerType, +>(containerA: TContainerAType, containerB: TContainerBType) { const { name: containerAName, component: ContainerA, @@ -133,7 +122,9 @@ export function createContainerFn( hook: useContainerB, } = CONTAINERS[containerB]; - function NewContainer(props: any) { + const Container = ( + props: ContainerProps & ContainerProps, + ) => { const { children: childrenA, props: propsA } = useContainerA(props); const { children: childrenB, props: propsB } = useContainerB({ ...propsA, @@ -141,15 +132,17 @@ export function createContainerFn( }); return {childrenB}; - } + }; - NewContainer.displayName = `Victory${containerAName}${containerBName}Container`; - NewContainer.role = "container"; - NewContainer.defaultEvents = (props: any) => + Container.displayName = `Victory${containerAName}${containerBName}Container`; + Container.role = "container"; + Container.defaultEvents = ( + props: ContainerProps & ContainerProps, + ) => combineDefaultEvents([ ...ContainerA.defaultEvents(props), ...ContainerB.defaultEvents(props), ]); - return NewContainer; + return Container; } From d2ebc2a72100985c58303dded34afe6c876e2652 Mon Sep 17 00:00:00 2001 From: Kenan Date: Wed, 14 Feb 2024 11:07:09 +0000 Subject: [PATCH 15/36] improve generic naming --- .../victory-create-container/src/create-container.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/victory-create-container/src/create-container.tsx b/packages/victory-create-container/src/create-container.tsx index d854cbb8f..ed1f2715c 100644 --- a/packages/victory-create-container/src/create-container.tsx +++ b/packages/victory-create-container/src/create-container.tsx @@ -108,9 +108,9 @@ type ContainerProps = React.ComponentProps< >; export function createContainer< - TContainerAType extends ContainerType, - TContainerBType extends ContainerType, ->(containerA: TContainerAType, containerB: TContainerBType) { + TContainerA extends ContainerType, + TContainerB extends ContainerType, +>(containerA: TContainerA, containerB: TContainerB) { const { name: containerAName, component: ContainerA, @@ -123,7 +123,7 @@ export function createContainer< } = CONTAINERS[containerB]; const Container = ( - props: ContainerProps & ContainerProps, + props: ContainerProps & ContainerProps, ) => { const { children: childrenA, props: propsA } = useContainerA(props); const { children: childrenB, props: propsB } = useContainerB({ @@ -137,7 +137,7 @@ export function createContainer< Container.displayName = `Victory${containerAName}${containerBName}Container`; Container.role = "container"; Container.defaultEvents = ( - props: ContainerProps & ContainerProps, + props: ContainerProps & ContainerProps, ) => combineDefaultEvents([ ...ContainerA.defaultEvents(props), From 0a8a23c8fc96f2cde540d08f3af0b238cc418f80 Mon Sep 17 00:00:00 2001 From: Kenan Date: Wed, 14 Feb 2024 12:22:28 +0000 Subject: [PATCH 16/36] rewrite victory-native/victory-container --- packages/victory-core/src/exports.test.ts | 2 + .../victory-container/victory-container.tsx | 95 +++++-- .../src/components/victory-container.tsx | 267 +++++++++--------- 3 files changed, 206 insertions(+), 158 deletions(-) diff --git a/packages/victory-core/src/exports.test.ts b/packages/victory-core/src/exports.test.ts index 4edfd4835..4778c4095 100644 --- a/packages/victory-core/src/exports.test.ts +++ b/packages/victory-core/src/exports.test.ts @@ -108,6 +108,7 @@ import { VictoryCommonProps, VictoryCommonThemeProps, VictoryContainer, + useVictoryContainer, VictoryContainerProps, VictoryDatableProps, VictoryLabel, @@ -187,6 +188,7 @@ describe("victory-core", () => { "VictoryAnimation", "VictoryClipContainer", "VictoryContainer", + 'useVictoryContainer', "VictoryLabel", "VictoryPortal", "VictoryTheme", diff --git a/packages/victory-core/src/victory-container/victory-container.tsx b/packages/victory-core/src/victory-container/victory-container.tsx index cf7d6b331..edbc43a24 100644 --- a/packages/victory-core/src/victory-container/victory-container.tsx +++ b/packages/victory-core/src/victory-container/victory-container.tsx @@ -50,28 +50,13 @@ const defaultProps = { role: "img", }; -export const VictoryContainer = (initialProps: VictoryContainerProps) => { +export function useVictoryContainer( + initialProps: TProps, +) { const props = { ...defaultProps, ...initialProps }; - const { - role, - title, - desc, - children, - className, - portalZIndex, - portalComponent, - width, - height, - style, - tabIndex, - responsive, - events, - ouiaId, - ouiaSafe, - ouiaType, - } = props; + const { title, desc, width, height, responsive } = props; - const containerRef = useRef(null); + const localContainerRef = useRef(null); const portalRef = useRef(null); @@ -103,26 +88,78 @@ export const VictoryContainer = (initialProps: VictoryContainerProps) => { .filter(Boolean) .join(" ") || undefined; - const handleWheel = (e: WheelEvent) => e.preventDefault(); + const titleId = getIdForElement("title"); + const descId = getIdForElement("desc"); + + return { + ...props, + titleId, + descId, + dimensions, + viewBox, + preserveAspectRatio, + ariaLabelledBy, + ariaDescribedBy, + userProps, + portalRef, + localContainerRef, + }; +} + +export const VictoryContainer = (initialProps: VictoryContainerProps) => { + const { + role, + title, + desc, + children, + className, + portalZIndex, + portalComponent, + width, + height, + style, + tabIndex, + responsive, + events, + ouiaId, + ouiaSafe, + ouiaType, + dimensions, + ariaDescribedBy, + ariaLabelledBy, + viewBox, + preserveAspectRatio, + userProps, + titleId, + descId, + portalRef, + containerRef, + localContainerRef, + } = useVictoryContainer(initialProps); React.useEffect(() => { // TODO check that this works - if (!props.events?.onWheel) return; + if (!events?.onWheel) return; + + const handleWheel = (e: WheelEvent) => e.preventDefault(); - const container = containerRef?.current; + const container = (containerRef as React.RefObject) + ?.current; container?.addEventListener("wheel", handleWheel); return () => { container?.removeEventListener("wheel", handleWheel); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return (
{ data-ouia-component-id={ouiaId} data-ouia-component-type={ouiaType} data-ouia-safe={ouiaSafe} - ref={mergeRefs([containerRef, props.containerRef])} + ref={mergeRefs([localContainerRef, containerRef])} > { {...userProps} {...events} > - {title ? {title} : null} - {desc ? {desc} : null} + {title ? {title} : null} + {desc ? {desc} : null} {children}
{ - panResponder: any; +export const VictoryContainer = (initialProps: VictoryContainerNativeProps) => { + const props = useVictoryContainer(initialProps); + const { + title, + desc, + width, + height, + dimensions, + children, + style, + className, + ouiaId, + ouiaSafe, + ouiaType, + ariaLabelledBy, + ariaDescribedBy, + portalZIndex, + viewBox, + preserveAspectRatio, + userProps, + portalRef, + containerRef, + events, + onTouchStart, + onTouchEnd, + localContainerRef, + disableContainerEvents, + } = props; + + const callOptionalEventCallback = (eventName, event) => { + const callback = get(events, eventName); + if (callback) { + event.persist(); // RN nativeEvent is reused. see https://fb.me/react-event-pooling + callback(event, props, "__unknownEventKey__", eventName); + } + }; + + const handleResponderGrant = (event) => { + if (onTouchStart) { + onTouchStart(event); + } + callOptionalEventCallback("onTouchStart", event); + }; - constructor(props) { - super(props); - this.panResponder = this.getResponder(); - } + const handleResponderMove = (event) => { + const { touches } = event.nativeEvent; + if (touches && touches.length === 2) { + callOptionalEventCallback("onTouchPinch", event); + } else { + callOptionalEventCallback("onTouchMove", event); + } + }; - getResponder() { + const handleResponderEnd = (event) => { + if (onTouchEnd) { + onTouchEnd(event); + } + callOptionalEventCallback("onTouchEnd", event); + }; + + const getResponder = () => { let shouldBlockNativeResponder = no; + const { + allowDrag, + allowDraw, + allowResize, + allowSelection, + allowPan, + allowZoom, + } = props as any; + if ( - this.props && - ((this.props as any).allowDrag || - (this.props as any).allowDraw || - (this.props as any).allowResize || - (this.props as any).allowSelection || - (this.props as any).allowPan || - (this.props as any).allowZoom) + allowDrag || + allowDraw || + allowResize || + allowSelection || + allowPan || + allowZoom ) { shouldBlockNativeResponder = yes; } return PanResponder.create({ onStartShouldSetPanResponder: yes, - onStartShouldSetPanResponderCapture: no, - onMoveShouldSetPanResponder: yes, - onMoveShouldSetPanResponderCapture: yes, - onShouldBlockNativeResponder: shouldBlockNativeResponder, - onPanResponderTerminationRequest: yes, - // User has started a touch move - onPanResponderGrant: this.handleResponderGrant.bind(this), - // Active touch or touches have moved - onPanResponderMove: this.handleResponderMove.bind(this), - // The user has released all touches - onPanResponderRelease: this.handleResponderEnd.bind(this), - // Another component has become the responder - onPanResponderTerminate: this.handleResponderEnd.bind(this), + onPanResponderGrant: handleResponderGrant, // User has started a touch move + onPanResponderMove: handleResponderMove, // Active touch or touches have moved + onPanResponderRelease: handleResponderEnd, // The user has released all touches + onPanResponderTerminate: handleResponderEnd, // Another component has become the responder }); - } - - callOptionalEventCallback(eventName, evt) { - const callback = get(this.props.events, eventName); - if (callback) { - evt.persist(); // RN nativeEvent is reused. see https://fb.me/react-event-pooling - callback(evt, this.props, "__unknownEventKey__", eventName); - } - } - - handleResponderGrant(evt) { - if (this.props.onTouchStart) { - this.props.onTouchStart(evt); - } - this.callOptionalEventCallback("onTouchStart", evt); - } - - handleResponderMove(evt) { - const { touches } = evt.nativeEvent; - if (touches && touches.length === 2) { - this.callOptionalEventCallback("onTouchPinch", evt); - } else { - this.callOptionalEventCallback("onTouchMove", evt); - } - } - - handleResponderEnd(evt) { - if (this.props.onTouchEnd) { - this.props.onTouchEnd(evt); - } - this.callOptionalEventCallback("onTouchEnd", evt); - } - - // Overrides method in victory-core - renderContainer(props, svgProps, style) { - const { - title, - desc, - className, - width, - height, - portalZIndex, - responsive, - disableContainerEvents, - } = props; - const children = this.getChildren(props); - const dimensions = responsive - ? { width: "100%", height: "100%" } - : { width, height }; - const baseStyle = NativeHelpers.getStyle(style, ["width", "height"]); - const divStyle = Object.assign({}, baseStyle, { position: "relative" }); - const portalDivStyle = { - zIndex: portalZIndex, - position: "absolute", - top: 0, - left: 0, - }; - const portalSvgStyle = Object.assign({ overflow: "visible" }, dimensions); - const portalProps = { - width, - height, - viewBox: svgProps.viewBox, - style: portalSvgStyle, - }; - const handlers = disableContainerEvents - ? {} - : this.panResponder.panHandlers; - return ( - + - + {title ? {title} : null} + {desc ? {desc} : null} + {children} + - {/* - The following Rect is a temporary solution until the following RNSVG issue is resolved - https://github.com/react-native-svg/react-native-svg/issues/1488 - */} - - {title ? {title} : null} - {desc ? {desc} : null} - {children} - - - - - - ); - } -} + + + + + ); +}; From 242279b609ff46bb4ef4d1839c6f957321e2ddb9 Mon Sep 17 00:00:00 2001 From: Kenan Date: Wed, 14 Feb 2024 14:44:53 +0000 Subject: [PATCH 17/36] victory-native/victory-brush-container --- .../src/victory-brush-container.tsx | 6 +- .../src/victory-cursor-container.tsx | 6 +- .../components/victory-brush-container.tsx | 102 ++++++++---------- .../src/victory-selection-container.tsx | 12 ++- .../src/victory-voronoi-container.tsx | 6 +- .../src/victory-zoom-container.tsx | 6 +- 6 files changed, 65 insertions(+), 73 deletions(-) diff --git a/packages/victory-brush-container/src/victory-brush-container.tsx b/packages/victory-brush-container/src/victory-brush-container.tsx index 8efa882be..084148ef6 100644 --- a/packages/victory-brush-container/src/victory-brush-container.tsx +++ b/packages/victory-brush-container/src/victory-brush-container.tsx @@ -38,7 +38,7 @@ export interface VictoryBrushContainerProps extends VictoryContainerProps { ) => void; } -const defaultProps = { +export const VICTORY_BRUSH_CONTAINER_DEFAULT_PROPS = { allowDrag: true, allowDraw: true, allowResize: true, @@ -60,7 +60,7 @@ const defaultProps = { export const useVictoryBrushContainer = ( initialProps: VictoryBrushContainerProps, ) => { - const props = { ...defaultProps, ...initialProps }; + const props = { ...VICTORY_BRUSH_CONTAINER_DEFAULT_PROPS, ...initialProps }; const { children } = props; const getSelectBox = (coordinates) => { @@ -181,7 +181,7 @@ VictoryBrushContainer.role = "container"; VictoryBrushContainer.defaultEvents = ( initialProps: VictoryBrushContainerProps, ) => { - const props = { ...defaultProps, ...initialProps }; + const props = { ...VICTORY_BRUSH_CONTAINER_DEFAULT_PROPS, ...initialProps }; const createEventHandler = ( handler: VictoryEventHandler, diff --git a/packages/victory-cursor-container/src/victory-cursor-container.tsx b/packages/victory-cursor-container/src/victory-cursor-container.tsx index 198c4046f..83771d0bc 100644 --- a/packages/victory-cursor-container/src/victory-cursor-container.tsx +++ b/packages/victory-cursor-container/src/victory-cursor-container.tsx @@ -29,7 +29,7 @@ export interface VictoryCursorContainerProps extends VictoryContainerProps { ) => void; } -const defaultProps = { +export const VICTORY_CURSOR_CONTAINER_DEFAULT_PROPS = { cursorLabelComponent: , cursorLabelOffset: { x: 5, @@ -41,7 +41,7 @@ const defaultProps = { export const useVictoryCursorContainer = ( initialProps: VictoryCursorContainerProps, ) => { - const props = { ...defaultProps, ...initialProps }; + const props = { ...VICTORY_CURSOR_CONTAINER_DEFAULT_PROPS, ...initialProps }; const { children } = props; const getCursorPosition = () => { @@ -185,7 +185,7 @@ VictoryCursorContainer.role = "container"; VictoryCursorContainer.defaultEvents = ( initialProps: VictoryCursorContainerProps, ) => { - const props = { ...defaultProps, ...initialProps }; + const props = { ...VICTORY_CURSOR_CONTAINER_DEFAULT_PROPS, ...initialProps }; const createEventHandler = (handler: VictoryEventHandler, disabled?: boolean): VictoryEventHandler => // eslint-disable-next-line max-params diff --git a/packages/victory-native/src/components/victory-brush-container.tsx b/packages/victory-native/src/components/victory-brush-container.tsx index 1b90bee72..d7bd00ac2 100644 --- a/packages/victory-native/src/components/victory-brush-container.tsx +++ b/packages/victory-native/src/components/victory-brush-container.tsx @@ -1,15 +1,15 @@ +/* eslint-disable react/no-multi-comp */ import React from "react"; import { Rect } from "react-native-svg"; -import { flow } from "lodash"; +import NativeHelpers from "../helpers/native-helpers"; +import { VictoryEventHandler } from "victory-core"; import { - VictoryBrushContainer as VictoryBrushContainerBase, BrushHelpers, - brushContainerMixin as originalBrushMixin, VictoryBrushContainerProps, + useVictoryBrushContainer, + VICTORY_BRUSH_CONTAINER_DEFAULT_PROPS, } from "victory-brush-container"; import { VictoryContainer } from "./victory-container"; -import NativeHelpers from "../helpers/native-helpers"; -import { VictoryEventHandler } from "victory-core"; export interface VictoryBrushContainerNativeProps extends VictoryBrushContainerProps { @@ -26,60 +26,46 @@ const RectWithStyle = ({ style?: Record; }) => ; -function nativeBrushMixin< - TBase extends React.ComponentClass, - TProps extends VictoryBrushContainerNativeProps, ->(Base: TBase) { - // @ts-expect-error "TS2545: A mixin class must have a constructor with a single rest parameter of type 'any[]'." - return class VictoryNativeBrushContainer extends Base { - // eslint-disable-line max-len - // assign native specific defaultProps over web `VictoryBrushContainer` defaultProps - static defaultProps = { - ...VictoryBrushContainerBase.defaultProps, - brushComponent: , - handleComponent: , - }; +export const VictoryNativeBrushContainer = ( + initialProps: VictoryBrushContainerNativeProps, +) => { + const props = useVictoryBrushContainer({ + ...initialProps, + brushComponent: , + handleComponent: , + }); + return ; +}; - // overrides all web events with native specific events - static defaultEvents = (props: TProps) => { - return [ - { - target: "parent", - eventHandlers: { - onTouchStart: (evt, targetProps) => { - if (props.disable) { - return {}; - } - BrushHelpers.onGlobalMouseMove.cancel(); - return BrushHelpers.onMouseDown(evt, targetProps); - }, - onTouchMove: (evt, targetProps) => { - return props.disable - ? {} - : BrushHelpers.onGlobalMouseMove(evt, targetProps); - }, - onTouchEnd: (evt, targetProps) => { - if (props.disable) { - return {}; - } - BrushHelpers.onGlobalMouseMove.cancel(); - return BrushHelpers.onGlobalMouseUp(evt, targetProps); - }, - }, - }, - ]; - }; - }; -} +VictoryNativeBrushContainer.role = "container"; -const combinedMixin: ( - base: React.ComponentClass, -) => React.ComponentClass = flow( - originalBrushMixin, - nativeBrushMixin, -); +VictoryNativeBrushContainer.defaultEvents = ( + initialProps: VictoryBrushContainerNativeProps, +) => { + const props = { ...VICTORY_BRUSH_CONTAINER_DEFAULT_PROPS, ...initialProps }; + const createEventHandler = + (handler: VictoryEventHandler, cancel: boolean): VictoryEventHandler => + // eslint-disable-next-line max-params + (event, targetProps, eventKey, context) => { + if (props.disable) { + return {}; + } -export const brushContainerMixin = (base: React.ComponentClass) => - combinedMixin(base); + if (cancel) { + BrushHelpers.onGlobalMouseMove.cancel(); + } + + return handler(event, { ...props, ...targetProps }, eventKey, context); + }; -export const VictoryBrushContainer = brushContainerMixin(VictoryContainer); + return [ + { + target: "parent", + eventHandlers: { + onTouchStart: createEventHandler(BrushHelpers.onMouseDown, true), + onTouchMove: createEventHandler(BrushHelpers.onGlobalMouseMove, false), + onTouchEnd: createEventHandler(BrushHelpers.onGlobalMouseUp, true), + }, + }, + ]; +}; diff --git a/packages/victory-selection-container/src/victory-selection-container.tsx b/packages/victory-selection-container/src/victory-selection-container.tsx index e4534b984..66c1f599a 100644 --- a/packages/victory-selection-container/src/victory-selection-container.tsx +++ b/packages/victory-selection-container/src/victory-selection-container.tsx @@ -32,7 +32,7 @@ export interface VictorySelectionContainerProps extends VictoryContainerProps { selectionStyle?: React.CSSProperties; } -const defaultProps = { +export const VICTORY_SELECTION_CONTAINER_DEFAULT_PROPS = { activateSelectedData: true, allowSelection: true, selectionComponent: , @@ -46,7 +46,10 @@ const defaultProps = { export const useVictorySelectionContainer = ( initialProps: VictorySelectionContainerProps, ) => { - const props = { ...defaultProps, ...initialProps }; + const props = { + ...VICTORY_SELECTION_CONTAINER_DEFAULT_PROPS, + ...initialProps, + }; const { x1, x2, y1, y2, selectionStyle, selectionComponent, children, name } = props; @@ -86,7 +89,10 @@ VictorySelectionContainer.role = "container"; VictorySelectionContainer.defaultEvents = ( initialProps: VictorySelectionContainerProps, ) => { - const props = { ...defaultProps, ...initialProps }; + const props = { + ...VICTORY_SELECTION_CONTAINER_DEFAULT_PROPS, + ...initialProps, + }; const createEventHandler = (handler: VictoryEventHandler, disabled?: boolean): VictoryEventHandler => // eslint-disable-next-line max-params diff --git a/packages/victory-voronoi-container/src/victory-voronoi-container.tsx b/packages/victory-voronoi-container/src/victory-voronoi-container.tsx index 5ed99f7d0..f79633112 100644 --- a/packages/victory-voronoi-container/src/victory-voronoi-container.tsx +++ b/packages/victory-voronoi-container/src/victory-voronoi-container.tsx @@ -26,7 +26,7 @@ export interface VictoryVoronoiContainerProps extends VictoryContainerProps { voronoiPadding?: PaddingProps; } -const defaultProps = { +export const VICTORY_VORONOI_CONTAINER_DEFAULT_PROPS = { activateData: true, activateLabels: true, labelComponent: , @@ -41,7 +41,7 @@ const getPoint = (point) => { export const useVictoryVoronoiContainer = ( initialProps: VictoryVoronoiContainerProps, ) => { - const props = { ...defaultProps, ...initialProps }; + const props = { ...VICTORY_VORONOI_CONTAINER_DEFAULT_PROPS, ...initialProps }; const { children } = props; const getDimension = () => { @@ -203,7 +203,7 @@ VictoryVoronoiContainer.role = "container"; VictoryVoronoiContainer.defaultEvents = ( initialProps: VictoryVoronoiContainerProps, ) => { - const props = { ...defaultProps, ...initialProps }; + const props = { ...VICTORY_VORONOI_CONTAINER_DEFAULT_PROPS, ...initialProps }; const createEventHandler = (handler: VictoryEventHandler, disabled?: boolean): VictoryEventHandler => // eslint-disable-next-line max-params diff --git a/packages/victory-zoom-container/src/victory-zoom-container.tsx b/packages/victory-zoom-container/src/victory-zoom-container.tsx index 57db103ac..3910437a8 100644 --- a/packages/victory-zoom-container/src/victory-zoom-container.tsx +++ b/packages/victory-zoom-container/src/victory-zoom-container.tsx @@ -34,7 +34,7 @@ export interface VictoryZoomContainerProps extends VictoryContainerProps { zoomDomain?: Partial; } -const defaultProps = { +export const VICTORY_ZOOM_CONTAINER_DEFAULT_PROPS = { clipContainerComponent: , allowPan: true, allowZoom: true, @@ -44,7 +44,7 @@ const defaultProps = { export const useVictoryZoomContainer = ( initialProps: VictoryZoomContainerProps, ) => { - const props = { ...defaultProps, ...initialProps }; + const props = { ...VICTORY_ZOOM_CONTAINER_DEFAULT_PROPS, ...initialProps }; const { children, currentDomain, @@ -192,7 +192,7 @@ VictoryZoomContainer.role = "container"; VictoryZoomContainer.defaultEvents = ( initialProps: VictoryZoomContainerProps, ) => { - const props = { ...defaultProps, ...initialProps }; + const props = { ...VICTORY_ZOOM_CONTAINER_DEFAULT_PROPS, ...initialProps }; const createEventHandler = (handler: VictoryEventHandler, disabled?: boolean): VictoryEventHandler => // eslint-disable-next-line max-params From 38a9b66c9e377fd58093cf843adfd87009d3becc Mon Sep 17 00:00:00 2001 From: Kenan Date: Wed, 14 Feb 2024 14:57:13 +0000 Subject: [PATCH 18/36] victory-native/victory-cursor-container --- .../components/victory-brush-container.tsx | 6 +- .../components/victory-cursor-container.tsx | 88 ++++++++----------- 2 files changed, 38 insertions(+), 56 deletions(-) diff --git a/packages/victory-native/src/components/victory-brush-container.tsx b/packages/victory-native/src/components/victory-brush-container.tsx index d7bd00ac2..ab3a7b3e7 100644 --- a/packages/victory-native/src/components/victory-brush-container.tsx +++ b/packages/victory-native/src/components/victory-brush-container.tsx @@ -1,7 +1,6 @@ /* eslint-disable react/no-multi-comp */ import React from "react"; import { Rect } from "react-native-svg"; -import NativeHelpers from "../helpers/native-helpers"; import { VictoryEventHandler } from "victory-core"; import { BrushHelpers, @@ -10,6 +9,7 @@ import { VICTORY_BRUSH_CONTAINER_DEFAULT_PROPS, } from "victory-brush-container"; import { VictoryContainer } from "./victory-container"; +import NativeHelpers from "../helpers/native-helpers"; export interface VictoryBrushContainerNativeProps extends VictoryBrushContainerProps { @@ -31,8 +31,8 @@ export const VictoryNativeBrushContainer = ( ) => { const props = useVictoryBrushContainer({ ...initialProps, - brushComponent: , - handleComponent: , + brushComponent: initialProps.brushComponent ?? , + handleComponent: initialProps.handleComponent ?? , }); return ; }; diff --git a/packages/victory-native/src/components/victory-cursor-container.tsx b/packages/victory-native/src/components/victory-cursor-container.tsx index 9893d1631..8985b0ab3 100644 --- a/packages/victory-native/src/components/victory-cursor-container.tsx +++ b/packages/victory-native/src/components/victory-cursor-container.tsx @@ -1,10 +1,9 @@ import React from "react"; -import { flow } from "lodash"; import { VictoryEventHandler } from "victory-core"; import { - VictoryCursorContainer as VictoryCursorContainerBase, + useVictoryCursorContainer, CursorHelpers, - cursorContainerMixin as originalCursorMixin, + VICTORY_CURSOR_CONTAINER_DEFAULT_PROPS, VictoryCursorContainerProps, } from "victory-cursor-container"; import { VictoryLabel } from "./victory-label"; @@ -18,56 +17,39 @@ export interface VictoryCursorContainerNativeProps onTouchEnd?: VictoryEventHandler; } -function nativeCursorMixin< - TBase extends React.ComponentClass, - TProps extends VictoryCursorContainerNativeProps, ->(Base: TBase) { - // @ts-expect-error "TS2545: A mixin class must have a constructor with a single rest parameter of type 'any[]'." - return class VictoryNativeCursorContainer extends Base { - static displayName = "VictoryCursorContainer"; - // assign native specific defaultProps over web `VictoryCursorContainer` defaultProps - static defaultProps = { - ...VictoryCursorContainerBase.defaultProps, - cursorLabelComponent: , - cursorComponent: , - }; +export const VictoryNativeCursorContainer = ( + initialProps: VictoryCursorContainerNativeProps, +) => { + const props = useVictoryCursorContainer({ + ...initialProps, + cursorLabelComponent: initialProps.cursorLabelComponent ?? , + cursorComponent: initialProps.cursorComponent ?? , + }); + return ; +}; - // overrides all web events with native specific events - static defaultEvents = (props: TProps) => { - return [ - { - target: "parent", - eventHandlers: { - onTouchStart: (evt, targetProps) => { - return props.disable - ? {} - : CursorHelpers.onMouseMove(evt, targetProps); - }, - onTouchMove: (evt, targetProps) => { - return props.disable - ? {} - : CursorHelpers.onMouseMove(evt, targetProps); - }, - onTouchEnd: (evt, targetProps) => { - return props.disable - ? {} - : CursorHelpers.onTouchEnd(evt, targetProps); - }, - }, - }, - ]; - }; - }; -} - -const combinedMixin: ( - base: React.ComponentClass, -) => React.ComponentClass = flow( - originalCursorMixin, - nativeCursorMixin, -); +VictoryNativeCursorContainer.role = "container"; -export const cursorContainerMixin = (base: React.ComponentClass) => - combinedMixin(base); +VictoryNativeCursorContainer.defaultEvents = ( + initialProps: VictoryCursorContainerNativeProps, +) => { + const props = { ...VICTORY_CURSOR_CONTAINER_DEFAULT_PROPS, ...initialProps }; + const createEventHandler = + (handler: VictoryEventHandler, disabled?: boolean): VictoryEventHandler => + // eslint-disable-next-line max-params + (event, targetProps, eventKey, context) => + disabled || props.disable + ? {} + : handler(event, { ...props, ...targetProps }, eventKey, context); -export const VictoryCursorContainer = cursorContainerMixin(VictoryContainer); + return [ + { + target: "parent", + eventHandlers: { + onTouchStart: createEventHandler(CursorHelpers.onMouseMove), + onTouchMove: createEventHandler(CursorHelpers.onMouseMove), + onTouchEnd: createEventHandler(CursorHelpers.onTouchEnd), + }, + }, + ]; +}; From b8efab24717b0aa71ef32863bd86527627fbfdbe Mon Sep 17 00:00:00 2001 From: Kenan Date: Wed, 14 Feb 2024 15:04:35 +0000 Subject: [PATCH 19/36] victory-native/victory-selection-container --- .../victory-selection-container.tsx | 102 ++++++++---------- 1 file changed, 46 insertions(+), 56 deletions(-) diff --git a/packages/victory-native/src/components/victory-selection-container.tsx b/packages/victory-native/src/components/victory-selection-container.tsx index 4b8c9fc4a..99d51e764 100644 --- a/packages/victory-native/src/components/victory-selection-container.tsx +++ b/packages/victory-native/src/components/victory-selection-container.tsx @@ -1,12 +1,12 @@ +/* eslint-disable react/no-multi-comp */ import React from "react"; -import { flow } from "lodash"; import { Rect } from "react-native-svg"; import { VictoryEventHandler } from "victory-core"; import { - VictorySelectionContainer as VictorySelectionContainerBase, SelectionHelpers, - selectionContainerMixin as originalSelectionMixin, VictorySelectionContainerProps, + VICTORY_SELECTION_CONTAINER_DEFAULT_PROPS, + useVictorySelectionContainer, } from "victory-selection-container"; import { VictoryContainer } from "./victory-container"; import NativeHelpers from "../helpers/native-helpers"; @@ -26,61 +26,51 @@ const DefaultSelectionComponent = ({ style?: Record; }) => ; -function nativeSelectionMixin< - TBase extends React.ComponentClass, - TProps extends VictorySelectionContainerNativeProps, ->(Base: TBase) { - // @ts-expect-error "TS2545: A mixin class must have a constructor with a single rest parameter of type 'any[]'." - return class VictoryNativeSelectionContainer extends Base { - // eslint-disable-line max-len - // assign native specific defaultProps over web `VictorySelectionContainer` defaultProps - static defaultProps = { - ...VictorySelectionContainerBase.defaultProps, - standalone: true, - selectionComponent: , - }; +export const VictoryNativeSelectionContainer = ( + initialProps: VictorySelectionContainerNativeProps, +) => { + const props = useVictorySelectionContainer({ + ...initialProps, + standalone: initialProps.standalone ?? true, + selectionComponent: initialProps.selectionComponent ?? ( + + ), + }); + return ; +}; - // overrides all web events with native specific events - static defaultEvents = (props: TProps) => { - return [ - { - target: "parent", - eventHandlers: { - onTouchStart: (evt, targetProps) => { - if (props.disable) { - return {}; - } - SelectionHelpers.onMouseMove.cancel(); - return SelectionHelpers.onMouseDown(evt, targetProps); - }, - onTouchMove: (evt, targetProps) => { - return props.disable - ? {} - : SelectionHelpers.onMouseMove(evt, targetProps); - }, - onTouchEnd: (evt, targetProps) => { - if (props.disable) { - return {}; - } - SelectionHelpers.onMouseMove.cancel(); - return SelectionHelpers.onMouseUp(evt, targetProps); - }, - }, - }, - ]; - }; +VictoryNativeSelectionContainer.role = "container"; + +VictoryNativeSelectionContainer.defaultEvents = ( + initialProps: VictorySelectionContainerNativeProps, +) => { + const props = { + ...VICTORY_SELECTION_CONTAINER_DEFAULT_PROPS, + ...initialProps, }; -} + const createEventHandler = + (handler: VictoryEventHandler, cancel: boolean): VictoryEventHandler => + // eslint-disable-next-line max-params + (event, targetProps, eventKey, context) => { + if (props.disable) { + return {}; + } -const combinedMixin: ( - base: React.ComponentClass, -) => React.ComponentClass = flow( - originalSelectionMixin, - nativeSelectionMixin, -); + if (cancel) { + SelectionHelpers.onMouseMove.cancel(); + } -export const selectionContainerMixin = (base: React.ComponentClass) => - combinedMixin(base); + return handler(event, { ...props, ...targetProps }, eventKey, context); + }; -export const VictorySelectionContainer = - selectionContainerMixin(VictoryContainer); + return [ + { + target: "parent", + eventHandlers: { + onTouchStart: createEventHandler(SelectionHelpers.onMouseMove, true), + onTouchMove: createEventHandler(SelectionHelpers.onMouseMove, false), + onTouchEnd: createEventHandler(SelectionHelpers.onMouseUp, true), + }, + }, + ]; +}; From 623a6995c68691f9a405fe2cf44df4c9442d4ab8 Mon Sep 17 00:00:00 2001 From: Kenan Date: Wed, 14 Feb 2024 15:11:27 +0000 Subject: [PATCH 20/36] victory-native/victory-voronoi-container --- .../components/victory-brush-container.tsx | 6 +- .../components/victory-cursor-container.tsx | 6 +- .../victory-selection-container.tsx | 6 +- .../components/victory-voronoi-container.tsx | 111 ++++++++---------- 4 files changed, 57 insertions(+), 72 deletions(-) diff --git a/packages/victory-native/src/components/victory-brush-container.tsx b/packages/victory-native/src/components/victory-brush-container.tsx index ab3a7b3e7..21357314b 100644 --- a/packages/victory-native/src/components/victory-brush-container.tsx +++ b/packages/victory-native/src/components/victory-brush-container.tsx @@ -26,7 +26,7 @@ const RectWithStyle = ({ style?: Record; }) => ; -export const VictoryNativeBrushContainer = ( +export const VictoryBrushContainer = ( initialProps: VictoryBrushContainerNativeProps, ) => { const props = useVictoryBrushContainer({ @@ -37,9 +37,9 @@ export const VictoryNativeBrushContainer = ( return ; }; -VictoryNativeBrushContainer.role = "container"; +VictoryBrushContainer.role = "container"; -VictoryNativeBrushContainer.defaultEvents = ( +VictoryBrushContainer.defaultEvents = ( initialProps: VictoryBrushContainerNativeProps, ) => { const props = { ...VICTORY_BRUSH_CONTAINER_DEFAULT_PROPS, ...initialProps }; diff --git a/packages/victory-native/src/components/victory-cursor-container.tsx b/packages/victory-native/src/components/victory-cursor-container.tsx index 8985b0ab3..d7d4c4688 100644 --- a/packages/victory-native/src/components/victory-cursor-container.tsx +++ b/packages/victory-native/src/components/victory-cursor-container.tsx @@ -17,7 +17,7 @@ export interface VictoryCursorContainerNativeProps onTouchEnd?: VictoryEventHandler; } -export const VictoryNativeCursorContainer = ( +export const VictoryCursorContainer = ( initialProps: VictoryCursorContainerNativeProps, ) => { const props = useVictoryCursorContainer({ @@ -28,9 +28,9 @@ export const VictoryNativeCursorContainer = ( return ; }; -VictoryNativeCursorContainer.role = "container"; +VictoryCursorContainer.role = "container"; -VictoryNativeCursorContainer.defaultEvents = ( +VictoryCursorContainer.defaultEvents = ( initialProps: VictoryCursorContainerNativeProps, ) => { const props = { ...VICTORY_CURSOR_CONTAINER_DEFAULT_PROPS, ...initialProps }; diff --git a/packages/victory-native/src/components/victory-selection-container.tsx b/packages/victory-native/src/components/victory-selection-container.tsx index 99d51e764..1a90a9882 100644 --- a/packages/victory-native/src/components/victory-selection-container.tsx +++ b/packages/victory-native/src/components/victory-selection-container.tsx @@ -26,7 +26,7 @@ const DefaultSelectionComponent = ({ style?: Record; }) => ; -export const VictoryNativeSelectionContainer = ( +export const VictorySelectionContainer = ( initialProps: VictorySelectionContainerNativeProps, ) => { const props = useVictorySelectionContainer({ @@ -39,9 +39,9 @@ export const VictoryNativeSelectionContainer = ( return ; }; -VictoryNativeSelectionContainer.role = "container"; +VictorySelectionContainer.role = "container"; -VictoryNativeSelectionContainer.defaultEvents = ( +VictorySelectionContainer.defaultEvents = ( initialProps: VictorySelectionContainerNativeProps, ) => { const props = { diff --git a/packages/victory-native/src/components/victory-voronoi-container.tsx b/packages/victory-native/src/components/victory-voronoi-container.tsx index 8a693593d..2635fe2cf 100644 --- a/packages/victory-native/src/components/victory-voronoi-container.tsx +++ b/packages/victory-native/src/components/victory-voronoi-container.tsx @@ -1,12 +1,11 @@ /* eslint-disable react/no-multi-comp */ import React from "react"; -import { flow } from "lodash"; import { VictoryEventHandler } from "victory-core"; import { - VictoryVoronoiContainer as VictoryVoronoiContainerBase, VictoryVoronoiContainerProps, VoronoiHelpers, - voronoiContainerMixin as originalVoronoiMixin, + useVictoryVoronoiContainer, + VICTORY_VORONOI_CONTAINER_DEFAULT_PROPS, } from "victory-voronoi-container"; import { VictoryContainer } from "./victory-container"; import { VictoryTooltip } from "./victory-tooltip"; @@ -18,67 +17,53 @@ export interface VictoryVoronoiContainerNativeProps onTouchEnd?: VictoryEventHandler; } -function nativeVoronoiMixin< - TBase extends React.ComponentClass, - TProps extends VictoryVoronoiContainerNativeProps, ->(Base: TBase) { - // @ts-expect-error "TS2545: A mixin class must have a constructor with a single rest parameter of type 'any[]'." - return class VictoryNativeVoronoiContainer extends Base { - // assign native specific defaultProps over web `VictoryVoronoiContainer` defaultProps - static defaultProps = { - ...VictoryVoronoiContainerBase.defaultProps, - activateData: true, - activateLabels: true, - labelComponent: , - voronoiPadding: 5, - }; +const DEFAULT_VORONOI_PADDING = 5; - // overrides all web events with native specific events - static defaultEvents = (props: TProps) => { - return [ - { - target: "parent", - eventHandlers: { - onTouchStart: (evt, targetProps) => { - return props.disable - ? {} - : VoronoiHelpers.onMouseMove(evt, targetProps); - }, - onTouchMove: (evt, targetProps) => { - return props.disable - ? {} - : VoronoiHelpers.onMouseMove(evt, targetProps); - }, - onTouchEnd: (evt, targetProps) => { - return props.disable - ? {} - : VoronoiHelpers.onMouseLeave(evt, targetProps); - }, - }, - }, - { - target: "data", - eventHandlers: props.disable - ? {} - : { - onTouchStart: () => null, - onTouchMove: () => null, - onTouchEnd: () => null, - }, - }, - ]; - }; - }; -} +export const VictoryVoronoiContainer = ( + initialProps: VictoryVoronoiContainerNativeProps, +) => { + const props = useVictoryVoronoiContainer({ + ...initialProps, + activateData: initialProps.activateData ?? true, + activateLabels: initialProps.activateLabels ?? true, + labelComponent: initialProps.labelComponent ?? , + voronoiPadding: initialProps.voronoiPadding ?? DEFAULT_VORONOI_PADDING, + }); + return ; +}; -const combinedMixin: ( - base: React.ComponentClass, -) => React.ComponentClass = flow( - originalVoronoiMixin, - nativeVoronoiMixin, -); +VictoryVoronoiContainer.role = "container"; -export const voronoiContainerMixin = (base: React.ComponentClass) => - combinedMixin(base); +VictoryVoronoiContainer.defaultEvents = ( + initialProps: VictoryVoronoiContainerNativeProps, +) => { + const props = { ...VICTORY_VORONOI_CONTAINER_DEFAULT_PROPS, ...initialProps }; + const createEventHandler = + (handler: VictoryEventHandler, disabled?: boolean): VictoryEventHandler => + // eslint-disable-next-line max-params + (event, targetProps, eventKey, context) => + disabled || props.disable + ? {} + : handler(event, { ...props, ...targetProps }, eventKey, context); -export const VictoryVoronoiContainer = voronoiContainerMixin(VictoryContainer); + return [ + { + target: "parent", + eventHandlers: { + onTouchStart: createEventHandler(VoronoiHelpers.onMouseMove), + onTouchMove: createEventHandler(VoronoiHelpers.onMouseMove), + onTouchEnd: createEventHandler(VoronoiHelpers.onMouseLeave), + }, + }, + { + target: "data", + eventHandlers: props.disable + ? {} + : { + onTouchStart: () => null, + onTouchMove: () => null, + onTouchEnd: () => null, + }, + }, + ]; +}; From c9bad963443f8158e12803694938c49df4a434d5 Mon Sep 17 00:00:00 2001 From: Kenan Date: Wed, 14 Feb 2024 15:16:34 +0000 Subject: [PATCH 21/36] victory-native/victory-zoom-container --- .../src/components/victory-zoom-container.tsx | 108 ++++++------------ 1 file changed, 38 insertions(+), 70 deletions(-) diff --git a/packages/victory-native/src/components/victory-zoom-container.tsx b/packages/victory-native/src/components/victory-zoom-container.tsx index 9b178a2ee..e3aae6bd0 100644 --- a/packages/victory-native/src/components/victory-zoom-container.tsx +++ b/packages/victory-native/src/components/victory-zoom-container.tsx @@ -1,12 +1,11 @@ -import React, { ComponentClass } from "react"; -import { flow } from "lodash"; +import React from "react"; import { VictoryContainer } from "./victory-container"; import { VictoryClipContainer } from "./victory-clip-container"; import { VictoryEventHandler } from "victory-core"; import { - VictoryZoomContainer as VictoryZoomContainerBase, VictoryZoomContainerProps, - zoomContainerMixin as originalZoomMixin, + useVictoryZoomContainer, + VICTORY_ZOOM_CONTAINER_DEFAULT_PROPS, } from "victory-zoom-container"; import NativeZoomHelpers from "../helpers/native-zoom-helpers"; @@ -17,72 +16,41 @@ export interface VictoryZoomContainerNativeProps onTouchEnd?: VictoryEventHandler; } -function nativeZoomMixin< - TBase extends ComponentClass, - TProps extends VictoryZoomContainerNativeProps, ->(Base: TBase) { - // @ts-expect-error "TS2545: A mixin class must have a constructor with a single rest parameter of type 'any[]'." - return class VictoryNativeZoomContainer extends Base { - // assign native specific defaultProps over web `VictoryZoomContainer` defaultProps - static defaultProps = { - ...VictoryZoomContainerBase.defaultProps, - clipContainerComponent: , - }; +export const VictoryZoomContainer = ( + initialProps: VictoryZoomContainerNativeProps, +) => { + const props = useVictoryZoomContainer({ + ...initialProps, + clipContainerComponent: initialProps.clipContainerComponent ?? ( + + ), + }); + return ; +}; - // overrides all web events with native specific events - static defaultEvents = (props: TProps) => { - const { disable } = props; - return [ - { - target: "parent", - eventHandlers: { - // eslint-disable-next-line max-params - onTouchStart: (evt, targetProps) => { - return disable - ? {} - : NativeZoomHelpers.onTouchStart(evt, targetProps); - }, - // eslint-disable-next-line max-params - onTouchMove: (evt, targetProps, eventKey, ctx) => { - return disable - ? {} - : NativeZoomHelpers.onTouchMove( - evt, - targetProps, - eventKey, - ctx, - ); - }, - // eslint-disable-next-line max-params - onTouchEnd: () => { - return disable ? {} : NativeZoomHelpers.onTouchEnd(); - }, - // eslint-disable-next-line max-params - onTouchPinch: (evt, targetProps, eventKey, ctx) => { - return disable - ? {} - : NativeZoomHelpers.onTouchPinch( - evt, - targetProps, - eventKey, - ctx, - ); - }, - }, - }, - ]; - }; - }; -} - -const combinedMixin: ( - base: React.ComponentClass, -) => React.ComponentClass = flow( - originalZoomMixin, - nativeZoomMixin, -); +VictoryZoomContainer.role = "container"; -export const zoomContainerMixin = (base: React.ComponentClass) => - combinedMixin(base); +VictoryZoomContainer.defaultEvents = ( + initialProps: VictoryZoomContainerNativeProps, +) => { + const props = { ...VICTORY_ZOOM_CONTAINER_DEFAULT_PROPS, ...initialProps }; + const createEventHandler = + (handler: VictoryEventHandler, disabled?: boolean): VictoryEventHandler => + // eslint-disable-next-line max-params + (event, targetProps, eventKey, context) => + disabled || props.disable + ? {} + : handler(event, { ...props, ...targetProps }, eventKey, context); -export const VictoryZoomContainer = zoomContainerMixin(VictoryContainer); + return [ + { + target: "parent", + eventHandlers: { + onTouchStart: createEventHandler(NativeZoomHelpers.onTouchStart), + onTouchMove: createEventHandler(NativeZoomHelpers.onTouchMove), + onTouchEnd: createEventHandler(NativeZoomHelpers.onTouchEnd), + onTouchPinch: createEventHandler(NativeZoomHelpers.onTouchPinch), + }, + }, + ]; +}; From 780371a45e963438ce091f32eee9f2a9ae3b94e1 Mon Sep 17 00:00:00 2001 From: Kenan Date: Thu, 15 Feb 2024 09:35:48 +0000 Subject: [PATCH 22/36] Improve create-container types --- .../src/create-container.tsx | 144 ++++++++++-------- .../src/components/victory-container.tsx | 2 + .../src/helpers/create-container.ts | 20 +-- 3 files changed, 93 insertions(+), 73 deletions(-) diff --git a/packages/victory-create-container/src/create-container.tsx b/packages/victory-create-container/src/create-container.tsx index ed1f2715c..4f131289d 100644 --- a/packages/victory-create-container/src/create-container.tsx +++ b/packages/victory-create-container/src/create-container.tsx @@ -73,76 +73,94 @@ const combineDefaultEvents = (defaultEvents: any[]) => { return events.filter(Boolean); }; -const CONTAINERS = { - zoom: { - name: "Zoom", - component: VictoryZoomContainer, - hook: useVictoryZoomContainer, - }, - selection: { - name: "Selection", - component: VictorySelectionContainer, - hook: useVictorySelectionContainer, - }, - brush: { - name: "Brush", - component: VictoryBrushContainer, - hook: useVictoryBrushContainer, - }, - cursor: { - name: "Cursor", - component: VictoryCursorContainer, - hook: useVictoryCursorContainer, - }, - voronoi: { - name: "Voronoi", - component: VictoryVoronoiContainer, - hook: useVictoryVoronoiContainer, - }, +export type ContainerType = + | "zoom" + | "selection" + | "brush" + | "cursor" + | "voronoi"; + +/** + * Container hooks are used to provide the container logic to the container components through props and a modified children object + * - These hooks contain shared logic for both web and Victory Native containers. + * - In this utility, we call multiple of these hooks with the props returned by the previous to combine the container logic. + */ +export const CONTAINER_HOOKS = { + zoom: useVictoryZoomContainer, + selection: useVictorySelectionContainer, + brush: useVictoryBrushContainer, + cursor: useVictoryCursorContainer, + voronoi: useVictoryVoronoiContainer, }; -export type ContainerType = keyof typeof CONTAINERS; +/** + * Container hooks are wrappers that return a VictoryContainer with the props provided by their respective hooks, and the modified children. + * - These containers are specific to the web. Victory Native has its own container components. + * - For this utility, we only need the container components to extract the defaultEvents. + */ +const CONTAINER_COMPONENTS = { + zoom: VictoryZoomContainer, + selection: VictorySelectionContainer, + brush: VictoryBrushContainer, + cursor: VictoryCursorContainer, + voronoi: VictoryVoronoiContainer, +}; -type ContainerProps = React.ComponentProps< - typeof CONTAINERS[T]["component"] +type ContainerComponents = Record< + ContainerType, + React.ComponentType & { + defaultEvents: (props: any) => any[]; + } >; -export function createContainer< - TContainerA extends ContainerType, - TContainerB extends ContainerType, ->(containerA: TContainerA, containerB: TContainerB) { - const { - name: containerAName, - component: ContainerA, - hook: useContainerA, - } = CONTAINERS[containerA]; - const { - name: containerBName, - component: ContainerB, - hook: useContainerB, - } = CONTAINERS[containerB]; +export function makeCreateContainerFunction< + TContainerComponents extends ContainerComponents, +>( + containerComponents: TContainerComponents, + VictoryContainerBase: typeof VictoryContainer, +) { + type ContainerProps = React.ComponentProps< + TContainerComponents[T] + >; - const Container = ( - props: ContainerProps & ContainerProps, - ) => { - const { children: childrenA, props: propsA } = useContainerA(props); - const { children: childrenB, props: propsB } = useContainerB({ - ...propsA, - children: childrenA, - }); + return function combineContainers< + TContainerA extends ContainerType, + TContainerB extends ContainerType, + >(containerA: TContainerA, containerB: TContainerB) { + const ContainerA = containerComponents[containerA]; + const ContainerB = containerComponents[containerB]; + const useContainerA = CONTAINER_HOOKS[containerA]; + const useContainerB = CONTAINER_HOOKS[containerB]; - return {childrenB}; - }; + const CombinedContainer = ( + props: ContainerProps & ContainerProps, + ) => { + const { children: childrenA, props: propsA } = useContainerA(props); + const { children: childrenB, props: propsB } = useContainerB({ + ...propsA, + children: childrenA, + }); + + return ( + {childrenB} + ); + }; - Container.displayName = `Victory${containerAName}${containerBName}Container`; - Container.role = "container"; - Container.defaultEvents = ( - props: ContainerProps & ContainerProps, - ) => - combineDefaultEvents([ - ...ContainerA.defaultEvents(props), - ...ContainerB.defaultEvents(props), - ]); + CombinedContainer.displayName = `Victory${containerA}${containerB}Container`; + CombinedContainer.role = "container"; + CombinedContainer.defaultEvents = ( + props: ContainerProps & ContainerProps, + ) => + combineDefaultEvents([ + ...ContainerA.defaultEvents(props), + ...ContainerB.defaultEvents(props), + ]); - return Container; + return CombinedContainer; + }; } + +export const createContainer = makeCreateContainerFunction( + CONTAINER_COMPONENTS, + VictoryContainer, +); diff --git a/packages/victory-native/src/components/victory-container.tsx b/packages/victory-native/src/components/victory-container.tsx index 506f4f9fa..e7ca531a3 100644 --- a/packages/victory-native/src/components/victory-container.tsx +++ b/packages/victory-native/src/components/victory-container.tsx @@ -174,3 +174,5 @@ export const VictoryContainer = (initialProps: VictoryContainerNativeProps) => { ); }; + +VictoryContainer.role = "container"; diff --git a/packages/victory-native/src/helpers/create-container.ts b/packages/victory-native/src/helpers/create-container.ts index 43faf216b..a7f959a56 100644 --- a/packages/victory-native/src/helpers/create-container.ts +++ b/packages/victory-native/src/helpers/create-container.ts @@ -1,18 +1,18 @@ import { makeCreateContainerFunction } from "victory-create-container"; import { VictoryContainer } from "../components/victory-container"; -import { zoomContainerMixin } from "../components/victory-zoom-container"; -import { voronoiContainerMixin } from "../components/victory-voronoi-container"; -import { selectionContainerMixin } from "../components/victory-selection-container"; -import { brushContainerMixin } from "../components/victory-brush-container"; -import { cursorContainerMixin } from "../components/victory-cursor-container"; +import { VictoryZoomContainer } from "../components/victory-zoom-container"; +import { VictoryVoronoiContainer } from "../components/victory-voronoi-container"; +import { VictorySelectionContainer } from "../components/victory-selection-container"; +import { VictoryBrushContainer } from "../components/victory-brush-container"; +import { VictoryCursorContainer } from "../components/victory-cursor-container"; export const createContainer = makeCreateContainerFunction( { - zoom: [zoomContainerMixin], - voronoi: [voronoiContainerMixin], - selection: [selectionContainerMixin], - brush: [brushContainerMixin], - cursor: [cursorContainerMixin], + zoom: VictoryZoomContainer, + voronoi: VictoryVoronoiContainer, + selection: VictorySelectionContainer, + brush: VictoryBrushContainer, + cursor: VictoryCursorContainer, }, VictoryContainer, ); From bef94729e8a3dcfc55294c10ad1a4f54de9f7af0 Mon Sep 17 00:00:00 2001 From: Kenan Date: Fri, 16 Feb 2024 10:52:04 +0000 Subject: [PATCH 23/36] improve types --- .../src/victory-brush-container.tsx | 11 ++++- .../src/create-container.tsx | 15 ++++--- .../src/victory-cursor-container.tsx | 43 +++++++++++++------ .../victory-selection-container.tsx | 1 + .../src/victory-selection-container.tsx | 12 +++++- .../src/victory-voronoi-container.tsx | 12 +++++- .../src/victory-zoom-container.tsx | 16 ++++++- packages/victory/src/victory.test.ts | 36 ++++++++++------ 8 files changed, 109 insertions(+), 37 deletions(-) diff --git a/packages/victory-brush-container/src/victory-brush-container.tsx b/packages/victory-brush-container/src/victory-brush-container.tsx index 084148ef6..344577671 100644 --- a/packages/victory-brush-container/src/victory-brush-container.tsx +++ b/packages/victory-brush-container/src/victory-brush-container.tsx @@ -38,6 +38,12 @@ export interface VictoryBrushContainerProps extends VictoryContainerProps { ) => void; } +interface VictoryBrushContainerMutatedProps extends VictoryBrushContainerProps { + domain: { x: DomainTuple; y: DomainTuple }; + currentDomain: { x: DomainTuple; y: DomainTuple } | undefined; + cachedBrushDomain: { x: DomainTuple; y: DomainTuple } | undefined; +} + export const VICTORY_BRUSH_CONTAINER_DEFAULT_PROPS = { allowDrag: true, allowDraw: true, @@ -60,7 +66,10 @@ export const VICTORY_BRUSH_CONTAINER_DEFAULT_PROPS = { export const useVictoryBrushContainer = ( initialProps: VictoryBrushContainerProps, ) => { - const props = { ...VICTORY_BRUSH_CONTAINER_DEFAULT_PROPS, ...initialProps }; + const props = { + ...VICTORY_BRUSH_CONTAINER_DEFAULT_PROPS, + ...(initialProps as VictoryBrushContainerMutatedProps), + }; const { children } = props; const getSelectBox = (coordinates) => { diff --git a/packages/victory-create-container/src/create-container.tsx b/packages/victory-create-container/src/create-container.tsx index 4f131289d..a5f6a6ee4 100644 --- a/packages/victory-create-container/src/create-container.tsx +++ b/packages/victory-create-container/src/create-container.tsx @@ -85,7 +85,7 @@ export type ContainerType = * - These hooks contain shared logic for both web and Victory Native containers. * - In this utility, we call multiple of these hooks with the props returned by the previous to combine the container logic. */ -export const CONTAINER_HOOKS = { +const CONTAINER_HOOKS = { zoom: useVictoryZoomContainer, selection: useVictorySelectionContainer, brush: useVictoryBrushContainer, @@ -136,13 +136,16 @@ export function makeCreateContainerFunction< props: ContainerProps & ContainerProps, ) => { const { children: childrenA, props: propsA } = useContainerA(props); - const { children: childrenB, props: propsB } = useContainerB({ - ...propsA, - children: childrenA, - }); + const { children: combinedChildren, props: combinedProps } = + useContainerB({ + ...propsA, + children: childrenA, + }); return ( - {childrenB} + + {combinedChildren} + ); }; diff --git a/packages/victory-cursor-container/src/victory-cursor-container.tsx b/packages/victory-cursor-container/src/victory-cursor-container.tsx index 83771d0bc..13e0b399a 100644 --- a/packages/victory-cursor-container/src/victory-cursor-container.tsx +++ b/packages/victory-cursor-container/src/victory-cursor-container.tsx @@ -9,6 +9,8 @@ import { LineSegment, VictoryContainer, VictoryEventHandler, + DomainTuple, + PaddingProps, } from "victory-core"; import { defaults, isObject } from "lodash"; import { CursorHelpers } from "./cursor-helpers"; @@ -23,12 +25,20 @@ export interface VictoryCursorContainerProps extends VictoryContainerProps { cursorLabelOffset?: CursorCoordinatesPropType; defaultCursorValue?: CursorCoordinatesPropType; disable?: boolean; + horizontal?: boolean; + padding?: PaddingProps; onCursorChange?: ( value: CursorCoordinatesPropType, props: VictoryCursorContainerProps, ) => void; } +interface VictoryCursorContainerMutatedProps + extends VictoryCursorContainerProps { + cursorValue: CoordinatesPropType | null; + domain: { x: DomainTuple; y: DomainTuple }; +} + export const VICTORY_CURSOR_CONTAINER_DEFAULT_PROPS = { cursorLabelComponent: , cursorLabelOffset: { @@ -41,7 +51,10 @@ export const VICTORY_CURSOR_CONTAINER_DEFAULT_PROPS = { export const useVictoryCursorContainer = ( initialProps: VictoryCursorContainerProps, ) => { - const props = { ...VICTORY_CURSOR_CONTAINER_DEFAULT_PROPS, ...initialProps }; + const props = { + ...VICTORY_CURSOR_CONTAINER_DEFAULT_PROPS, + ...(initialProps as VictoryCursorContainerMutatedProps), + }; const { children } = props; const getCursorPosition = () => { @@ -52,9 +65,9 @@ export const useVictoryCursorContainer = ( if (typeof defaultCursorValue === "number") { return { - x: (domain.x[0] + domain.x[1]) / 2, - y: (domain.y[0] + domain.y[1]) / 2, - [cursorDimension]: defaultCursorValue, + x: ((domain.x[0] as number) + (domain.x[1] as number)) / 2, + y: ((domain.y[0] as number) + (domain.y[1] as number)) / 2, + ...(cursorDimension ? { [cursorDimension]: defaultCursorValue } : {}), }; } @@ -76,10 +89,12 @@ export const useVictoryCursorContainer = ( const getPadding = () => { if (props.padding === undefined) { - const child = props.children.find((c) => { - return isObject(c.props) && c.props.padding !== undefined; - }); - return Helpers.getPadding(child.props); + const child = Array.isArray(props.children) + ? props.children.find((c: any) => { + return isObject(c.props) && c.props.padding !== undefined; + }) + : props.children; + return Helpers.getPadding(child?.props); } return Helpers.getPadding(props); }; @@ -108,13 +123,13 @@ export const useVictoryCursorContainer = ( const newElements: React.ReactElement[] = []; const padding = getPadding(); const cursorCoordinates = { - x: horizontal ? scale.y(cursorValue.y) : scale.x(cursorValue.x), - y: horizontal ? scale.x(cursorValue.x) : scale.y(cursorValue.y), + x: horizontal ? scale?.y?.(cursorValue.y) : scale?.x?.(cursorValue.x), + y: horizontal ? scale?.x?.(cursorValue.x) : scale?.y?.(cursorValue.y), }; if (cursorLabel) { let labelProps = defaults({ active: true }, cursorLabelComponent.props, { - x: cursorCoordinates.x + cursorLabelOffset.x, - y: cursorCoordinates.y + cursorLabelOffset.y, + x: cursorCoordinates.x || 0 + cursorLabelOffset.x, + y: cursorCoordinates.y || 0 + cursorLabelOffset.y, datum: cursorValue, active: true, key: `${name}-cursor-label`, @@ -144,7 +159,7 @@ export const useVictoryCursorContainer = ( x1: cursorCoordinates.x, x2: cursorCoordinates.x, y1: padding.top, - y2: height - padding.bottom, + y2: height || 0 - padding.bottom, style: cursorStyle, }), ); @@ -154,7 +169,7 @@ export const useVictoryCursorContainer = ( React.cloneElement(cursorComponent, { key: `${name}-y-cursor`, x1: padding.left, - x2: width - padding.right, + x2: width || 0 - padding.right, y1: cursorCoordinates.y, y2: cursorCoordinates.y, style: cursorStyle, diff --git a/packages/victory-native/src/components/victory-selection-container.tsx b/packages/victory-native/src/components/victory-selection-container.tsx index 1a90a9882..4c0f0fcc7 100644 --- a/packages/victory-native/src/components/victory-selection-container.tsx +++ b/packages/victory-native/src/components/victory-selection-container.tsx @@ -31,6 +31,7 @@ export const VictorySelectionContainer = ( ) => { const props = useVictorySelectionContainer({ ...initialProps, + // @ts-expect-error TODO: standalone is not a valid prop for VictoryContainer, figure out why this is here standalone: initialProps.standalone ?? true, selectionComponent: initialProps.selectionComponent ?? ( diff --git a/packages/victory-selection-container/src/victory-selection-container.tsx b/packages/victory-selection-container/src/victory-selection-container.tsx index 66c1f599a..d6cd2c1ec 100644 --- a/packages/victory-selection-container/src/victory-selection-container.tsx +++ b/packages/victory-selection-container/src/victory-selection-container.tsx @@ -43,12 +43,20 @@ export const VICTORY_SELECTION_CONTAINER_DEFAULT_PROPS = { }, }; +interface VictorySelectionContainerMutatedProps + extends VictorySelectionContainerProps { + x1: number; + x2: number; + y1: number; + y2: number; +} + export const useVictorySelectionContainer = ( initialProps: VictorySelectionContainerProps, ) => { const props = { ...VICTORY_SELECTION_CONTAINER_DEFAULT_PROPS, - ...initialProps, + ...(initialProps as VictorySelectionContainerMutatedProps), }; const { x1, x2, y1, y2, selectionStyle, selectionComponent, children, name } = @@ -73,7 +81,7 @@ export const useVictorySelectionContainer = ( height, style: selectionStyle, }), - ], + ] as React.ReactElement[], }; }; diff --git a/packages/victory-voronoi-container/src/victory-voronoi-container.tsx b/packages/victory-voronoi-container/src/victory-voronoi-container.tsx index f79633112..c17be8d13 100644 --- a/packages/victory-voronoi-container/src/victory-voronoi-container.tsx +++ b/packages/victory-voronoi-container/src/victory-voronoi-container.tsx @@ -24,6 +24,13 @@ export interface VictoryVoronoiContainerProps extends VictoryContainerProps { voronoiBlacklist?: (string | RegExp)[]; voronoiDimension?: "x" | "y"; voronoiPadding?: PaddingProps; + horizontal?: boolean; +} + +interface VictoryVoronoiContainerMutatedProps + extends VictoryVoronoiContainerProps { + mousePosition: { x: number; y: number }; + activePoints: any[]; } export const VICTORY_VORONOI_CONTAINER_DEFAULT_PROPS = { @@ -41,7 +48,10 @@ const getPoint = (point) => { export const useVictoryVoronoiContainer = ( initialProps: VictoryVoronoiContainerProps, ) => { - const props = { ...VICTORY_VORONOI_CONTAINER_DEFAULT_PROPS, ...initialProps }; + const props = { + ...VICTORY_VORONOI_CONTAINER_DEFAULT_PROPS, + ...(initialProps as VictoryVoronoiContainerMutatedProps), + }; const { children } = props; const getDimension = () => { diff --git a/packages/victory-zoom-container/src/victory-zoom-container.tsx b/packages/victory-zoom-container/src/victory-zoom-container.tsx index 3910437a8..f2f69b58b 100644 --- a/packages/victory-zoom-container/src/victory-zoom-container.tsx +++ b/packages/victory-zoom-container/src/victory-zoom-container.tsx @@ -32,6 +32,17 @@ export interface VictoryZoomContainerProps extends VictoryContainerProps { ) => void; zoomDimension?: ZoomDimensionType; zoomDomain?: Partial; + horizontal?: boolean; +} + +interface VictoryZoomContainerMutatedProps extends VictoryZoomContainerProps { + domain: ZoomDomain; + originalDomain: ZoomDomain; + currentDomain: ZoomDomain; + cachedZoomDomain: ZoomDomain; + scale: any; + polar: boolean; + origin: { x: number; y: number }; } export const VICTORY_ZOOM_CONTAINER_DEFAULT_PROPS = { @@ -44,7 +55,10 @@ export const VICTORY_ZOOM_CONTAINER_DEFAULT_PROPS = { export const useVictoryZoomContainer = ( initialProps: VictoryZoomContainerProps, ) => { - const props = { ...VICTORY_ZOOM_CONTAINER_DEFAULT_PROPS, ...initialProps }; + const props = { + ...VICTORY_ZOOM_CONTAINER_DEFAULT_PROPS, + ...(initialProps as VictoryZoomContainerMutatedProps), + }; const { children, currentDomain, diff --git a/packages/victory/src/victory.test.ts b/packages/victory/src/victory.test.ts index bb0e16e03..3ea13f275 100644 --- a/packages/victory/src/victory.test.ts +++ b/packages/victory/src/victory.test.ts @@ -136,15 +136,21 @@ import { Wrapper, ZoomHelpers, addEvents, - brushContainerMixin, - combineContainerMixins, createContainer, - cursorContainerMixin, makeCreateContainerFunction, - selectionContainerMixin, useCanvasContext, - voronoiContainerMixin, - zoomContainerMixin, + useVictoryBrushContainer, + useVictoryCursorContainer, + useVictorySelectionContainer, + useVictoryVoronoiContainer, + useVictoryZoomContainer, + VICTORY_BRUSH_CONTAINER_DEFAULT_PROPS, + VICTORY_CURSOR_CONTAINER_DEFAULT_PROPS, + VICTORY_SELECTION_CONTAINER_DEFAULT_PROPS, + VICTORY_VORONOI_CONTAINER_DEFAULT_PROPS, + VICTORY_ZOOM_CONTAINER_DEFAULT_PROPS, + mergeRefs, + useVictoryContainer, } from "./index"; describe("victory", () => { @@ -344,6 +350,11 @@ describe("victory", () => { "TimerContext", "Transitions", "UserProps", + "VICTORY_BRUSH_CONTAINER_DEFAULT_PROPS", + "VICTORY_CURSOR_CONTAINER_DEFAULT_PROPS", + "VICTORY_SELECTION_CONTAINER_DEFAULT_PROPS", + "VICTORY_VORONOI_CONTAINER_DEFAULT_PROPS", + "VICTORY_ZOOM_CONTAINER_DEFAULT_PROPS", "VictoryAccessibleGroup", "VictoryAnimation", "VictoryArea", @@ -382,10 +393,7 @@ describe("victory", () => { "Wrapper", "ZoomHelpers", "addEvents", - "brushContainerMixin", - "combineContainerMixins", "createContainer", - "cursorContainerMixin", "getBarPath", "getBarPosition", "getBarWidth", @@ -397,10 +405,14 @@ describe("victory", () => { "getVerticalBarPath", "getVerticalPolarBarPath", "makeCreateContainerFunction", - "selectionContainerMixin", + "mergeRefs", "useCanvasContext", - "voronoiContainerMixin", - "zoomContainerMixin", + "useVictoryBrushContainer", + "useVictoryContainer", + "useVictoryCursorContainer", + "useVictorySelectionContainer", + "useVictoryVoronoiContainer", + "useVictoryZoomContainer", ] `); }); From a1ea866996adbc5008491bd16bb02c2369c6aaf8 Mon Sep 17 00:00:00 2001 From: Kenan Date: Fri, 16 Feb 2024 14:57:31 +0000 Subject: [PATCH 24/36] refactor victory-portal and portal components --- .../victory-container/victory-container.tsx | 15 +-- .../src/victory-portal/portal-context.ts | 9 +- .../src/victory-portal/portal.tsx | 48 +------- .../src/victory-portal/victory-portal.tsx | 103 ++++++------------ .../src/components/victory-container.tsx | 100 +++++++++-------- .../src/components/victory-portal/portal.tsx | 14 +-- .../victory-portal/victory-portal.tsx | 22 ++-- 7 files changed, 119 insertions(+), 192 deletions(-) diff --git a/packages/victory-core/src/victory-container/victory-container.tsx b/packages/victory-core/src/victory-container/victory-container.tsx index edbc43a24..e12e13b53 100644 --- a/packages/victory-core/src/victory-container/victory-container.tsx +++ b/packages/victory-core/src/victory-container/victory-container.tsx @@ -58,7 +58,9 @@ export function useVictoryContainer( const localContainerRef = useRef(null); - const portalRef = useRef(null); + const [portalElement, setPortalElement] = React.useState(); + + const portalRef = (element: SVGSVGElement) => setPortalElement(element); // Generated ID stored in ref because it needs to persist across renders const generatedId = useRef(uniqueId("victory-container-")); @@ -102,6 +104,7 @@ export function useVictoryContainer( ariaDescribedBy, userProps, portalRef, + portalElement, localContainerRef, }; } @@ -132,6 +135,7 @@ export const VictoryContainer = (initialProps: VictoryContainerProps) => { userProps, titleId, descId, + portalElement, portalRef, containerRef, localContainerRef, @@ -154,14 +158,7 @@ export const VictoryContainer = (initialProps: VictoryContainerProps) => { }, []); return ( - +
(undefined); + PortalContext.displayName = "PortalContext"; diff --git a/packages/victory-core/src/victory-portal/portal.tsx b/packages/victory-core/src/victory-portal/portal.tsx index 3195394f4..116e8acd4 100644 --- a/packages/victory-core/src/victory-portal/portal.tsx +++ b/packages/victory-core/src/victory-portal/portal.tsx @@ -1,6 +1,4 @@ import React from "react"; -import { keys } from "lodash"; -import { PortalContextValue } from "./portal-context"; export interface PortalProps { className?: string; @@ -10,44 +8,8 @@ export interface PortalProps { width?: number; } -export class Portal - extends React.Component - implements PortalContextValue -{ - static displayName = "Portal"; - - private readonly map: Record; - private index: number; - - constructor(props: PortalProps) { - super(props); - this.map = {}; - this.index = 1; - } - - public portalRegister = (): number => { - return ++this.index; - }; - - public portalUpdate = (key: number, element: React.ReactElement) => { - this.map[key] = element; - this.forceUpdate(); - }; - - public portalDeregister = (key: number) => { - delete this.map[key]; - this.forceUpdate(); - }; - - public getChildren() { - return keys(this.map).map((key) => { - const el = this.map[key]; - return el ? React.cloneElement(el, { key }) : el; - }); - } - - // Overridden in victory-core-native - render() { - return {this.getChildren()}; - } -} +export const Portal = React.forwardRef( + (props, ref) => { + return ; + }, +); diff --git a/packages/victory-core/src/victory-portal/victory-portal.tsx b/packages/victory-core/src/victory-portal/victory-portal.tsx index 659f22608..30a6ae356 100644 --- a/packages/victory-core/src/victory-portal/victory-portal.tsx +++ b/packages/victory-core/src/victory-portal/victory-portal.tsx @@ -3,84 +3,45 @@ import { defaults } from "lodash"; import * as Log from "../victory-util/log"; import * as Helpers from "../victory-util/helpers"; import { PortalContext } from "./portal-context"; +import { createPortal } from "react-dom"; export interface VictoryPortalProps { children?: React.ReactElement; groupComponent?: React.ReactElement; } -export interface VictoryPortal { - context: React.ContextType; -} - -export class VictoryPortal extends React.Component { - static displayName = "VictoryPortal"; - - static role = "portal"; - - static defaultProps = { - groupComponent: , - }; - - static contextType = PortalContext; - private checkedContext!: boolean; - private portalKey!: number; - public renderInPlace!: boolean; - public element!: React.ReactElement; - - componentDidMount() { - if (!this.checkedContext) { - if (typeof this.context.portalUpdate !== "function") { - const msg = - "`renderInPortal` is not supported outside of `VictoryContainer`. " + - "Component will be rendered in place"; - Log.warn(msg); - this.renderInPlace = true; - } - this.checkedContext = true; - } - this.forceUpdate(); - } - - componentDidUpdate() { - if (!this.renderInPlace) { - this.portalKey = this.portalKey || this.context.portalRegister(); - this.context.portalUpdate(this.portalKey, this.element); - } - } +const defaultProps: VictoryPortalProps = { + groupComponent: , +}; - componentWillUnmount() { - if (this.context && this.context.portalDeregister) { - this.context.portalDeregister(this.portalKey); - } - } +export const VictoryPortal: React.FC = (initialProps) => { + const props = { ...defaultProps, ...initialProps }; + const portalContext = React.useContext(PortalContext); - // Overridden in victory-core-native - renderPortal(child: React.ReactElement) { - if (this.renderInPlace) { - return child; - } - this.element = child; - return null; + if (!portalContext) { + const msg = + "`renderInPortal` is not supported outside of `VictoryContainer`. " + + "Component will be rendered in place"; + Log.warn(msg); } - render() { - const children = ( - Array.isArray(this.props.children) - ? this.props.children[0] - : this.props.children - ) as React.ReactElement; - const { groupComponent } = this.props; - const childProps = (children && children.props) || {}; - const standardProps = childProps.groupComponent - ? { groupComponent, standalone: false } - : {}; - const newProps = defaults( - standardProps, - childProps, - Helpers.omit(this.props, ["children", "groupComponent"]), - ); - const child = children && React.cloneElement(children, newProps); - return this.renderPortal(child); - } -} + const children = ( + Array.isArray(props.children) ? props.children[0] : props.children + ) as React.ReactElement; + const { groupComponent } = props; + const childProps = (children && children.props) || {}; + const standardProps = childProps.groupComponent + ? { groupComponent, standalone: false } + : {}; + const newProps = defaults( + standardProps, + childProps, + Helpers.omit(props, ["children", "groupComponent"]), + ); + const child = children && React.cloneElement(children, newProps); + + // If there is no portal context, render the child in place + return portalContext?.portalElement + ? createPortal(child, portalContext.portalElement) + : child; +}; diff --git a/packages/victory-native/src/components/victory-container.tsx b/packages/victory-native/src/components/victory-container.tsx index e7ca531a3..d63763013 100644 --- a/packages/victory-native/src/components/victory-container.tsx +++ b/packages/victory-native/src/components/victory-container.tsx @@ -7,6 +7,7 @@ import { VictoryEventHandler, mergeRefs, useVictoryContainer, + PortalContext, } from "victory-core/es"; import NativeHelpers from "../helpers/native-helpers"; import { Portal } from "./victory-portal/portal"; @@ -41,6 +42,7 @@ export const VictoryContainer = (initialProps: VictoryContainerNativeProps) => { preserveAspectRatio, userProps, portalRef, + portalElement, containerRef, events, onTouchStart, @@ -122,56 +124,58 @@ export const VictoryContainer = (initialProps: VictoryContainerNativeProps) => { const baseStyle = NativeHelpers.getStyle(style, ["width", "height"]); return ( - - + - {/* The following Rect is a temporary solution until the following RNSVG issue is resolved https://github.com/react-native-svg/react-native-svg/issues/1488 */} - - {title ? {title} : null} - {desc ? {desc} : null} - {children} - - - - - + {/* The following Rect is a temporary solution until the following RNSVG issue is resolved https://github.com/react-native-svg/react-native-svg/issues/1488 */} + + {title ? {title} : null} + {desc ? {desc} : null} + {children} + + + + + + ); }; diff --git a/packages/victory-native/src/components/victory-portal/portal.tsx b/packages/victory-native/src/components/victory-portal/portal.tsx index ac1920c65..c5423d54d 100644 --- a/packages/victory-native/src/components/victory-portal/portal.tsx +++ b/packages/victory-native/src/components/victory-portal/portal.tsx @@ -1,9 +1,9 @@ -import React from "react"; +import React, { LegacyRef } from "react"; import Svg from "react-native-svg"; -import { Portal as PortalBase } from "victory-core/es"; +import { PortalProps } from "victory-core/es"; -export class Portal extends PortalBase { - render() { - return {this.getChildren()}; - } -} +export const Portal = React.forwardRef( + (props, ref) => { + return } {...props} />; + }, +); diff --git a/packages/victory-native/src/components/victory-portal/victory-portal.tsx b/packages/victory-native/src/components/victory-portal/victory-portal.tsx index 392c8bd9b..0130a9ab1 100644 --- a/packages/victory-native/src/components/victory-portal/victory-portal.tsx +++ b/packages/victory-native/src/components/victory-portal/victory-portal.tsx @@ -1,13 +1,15 @@ import React from "react"; import { G } from "react-native-svg"; -import { VictoryPortal as VictoryPortalBase } from "victory-core/es"; +import { + VictoryPortal as VictoryPortalBase, + VictoryPortalProps, +} from "victory-core/es"; -export class VictoryPortal extends VictoryPortalBase { - renderPortal(child) { - if (this.renderInPlace) { - return child || ; - } - this.element = child; - return ; - } -} +export const VictoryPortal = (initialProps: VictoryPortalProps) => { + return ( + } + /> + ); +}; From 03d3045e09554eb698d7ec486f30f235c9aa0795 Mon Sep 17 00:00:00 2001 From: Kenan Date: Fri, 16 Feb 2024 15:15:47 +0000 Subject: [PATCH 25/36] fix some tests --- demo/ts/components/ouia-demo.tsx | 2 ++ packages/victory-core/src/exports.test.ts | 2 +- .../victory-core/src/victory-container/victory-container.tsx | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/demo/ts/components/ouia-demo.tsx b/demo/ts/components/ouia-demo.tsx index e17f48e8a..75eb29260 100644 --- a/demo/ts/components/ouia-demo.tsx +++ b/demo/ts/components/ouia-demo.tsx @@ -27,6 +27,8 @@ class OuiaDemo extends React.Component { { "VictoryAnimation", "VictoryClipContainer", "VictoryContainer", - 'useVictoryContainer', "VictoryLabel", "VictoryPortal", "VictoryTheme", @@ -197,6 +196,7 @@ describe("victory-core", () => { "Wrapper", "addEvents", "mergeRefs", + "useVictoryContainer", ] `); }); diff --git a/packages/victory-core/src/victory-container/victory-container.tsx b/packages/victory-core/src/victory-container/victory-container.tsx index e12e13b53..be749d33e 100644 --- a/packages/victory-core/src/victory-container/victory-container.tsx +++ b/packages/victory-core/src/victory-container/victory-container.tsx @@ -64,7 +64,7 @@ export function useVictoryContainer( // Generated ID stored in ref because it needs to persist across renders const generatedId = useRef(uniqueId("victory-container-")); - const containerId = props.containerId ?? generatedId; + const containerId = props.containerId ?? generatedId.current; const getIdForElement = (elName: string) => `${containerId}-${elName}`; From f7bd26d6d0dc89544291b20c75be00ab0b92afa7 Mon Sep 17 00:00:00 2001 From: Kenan Date: Fri, 16 Feb 2024 15:25:35 +0000 Subject: [PATCH 26/36] fix more tests --- .../victory-container/victory-container.test.tsx | 15 +++++++++++---- .../src/victory-portal/victory-portal.tsx | 4 +++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/victory-core/src/victory-container/victory-container.test.tsx b/packages/victory-core/src/victory-container/victory-container.test.tsx index bb194e607..2623f23d1 100644 --- a/packages/victory-core/src/victory-container/victory-container.test.tsx +++ b/packages/victory-core/src/victory-container/victory-container.test.tsx @@ -18,10 +18,15 @@ describe("components/victory-container", () => { }); it("renders an svg with a title node", () => { - const { container } = render(); + const { container } = render( + , + ); expect(container.querySelector("title")).toMatchInlineSnapshot(` Victory Chart @@ -29,10 +34,12 @@ describe("components/victory-container", () => { }); it("renders an svg with a desc node", () => { - const { container } = render(); + const { container } = render( + , + ); expect(container.querySelector("desc")).toMatchInlineSnapshot(` description diff --git a/packages/victory-core/src/victory-portal/victory-portal.tsx b/packages/victory-core/src/victory-portal/victory-portal.tsx index 30a6ae356..b6f57ae5d 100644 --- a/packages/victory-core/src/victory-portal/victory-portal.tsx +++ b/packages/victory-core/src/victory-portal/victory-portal.tsx @@ -14,7 +14,7 @@ const defaultProps: VictoryPortalProps = { groupComponent: , }; -export const VictoryPortal: React.FC = (initialProps) => { +export const VictoryPortal = (initialProps: VictoryPortalProps) => { const props = { ...defaultProps, ...initialProps }; const portalContext = React.useContext(PortalContext); @@ -45,3 +45,5 @@ export const VictoryPortal: React.FC = (initialProps) => { ? createPortal(child, portalContext.portalElement) : child; }; + +VictoryPortal.role = "portal"; From 8f90ab5cf738fdc505bb89624182960af568581e Mon Sep 17 00:00:00 2001 From: Kenan Date: Fri, 16 Feb 2024 16:24:47 +0000 Subject: [PATCH 27/36] reset demo file --- demo/ts/components/ouia-demo.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/demo/ts/components/ouia-demo.tsx b/demo/ts/components/ouia-demo.tsx index 75eb29260..e17f48e8a 100644 --- a/demo/ts/components/ouia-demo.tsx +++ b/demo/ts/components/ouia-demo.tsx @@ -27,8 +27,6 @@ class OuiaDemo extends React.Component { Date: Mon, 19 Feb 2024 11:12:51 +0000 Subject: [PATCH 28/36] changeset --- .changeset/giant-carrots-own.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .changeset/giant-carrots-own.md diff --git a/.changeset/giant-carrots-own.md b/.changeset/giant-carrots-own.md new file mode 100644 index 000000000..be1c7a6a8 --- /dev/null +++ b/.changeset/giant-carrots-own.md @@ -0,0 +1,13 @@ +--- +"victory": patch +"victory-brush-container": patch +"victory-core": patch +"victory-create-container": patch +"victory-cursor-container": patch +"victory-native": patch +"victory-selection-container": patch +"victory-voronoi-container": patch +"victory-zoom-container": patch +--- + +Refactor containers and portal to function components From 06ac3f14dbe2dab0f3fc9f903ea5dd751f287e88 Mon Sep 17 00:00:00 2001 From: Kenan Date: Mon, 19 Feb 2024 11:26:04 +0000 Subject: [PATCH 29/36] fix cursor label position --- .../src/victory-cursor-container.tsx | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/victory-cursor-container/src/victory-cursor-container.tsx b/packages/victory-cursor-container/src/victory-cursor-container.tsx index 13e0b399a..208b0da9d 100644 --- a/packages/victory-cursor-container/src/victory-cursor-container.tsx +++ b/packages/victory-cursor-container/src/victory-cursor-container.tsx @@ -1,3 +1,4 @@ +/* eslint-disable complexity */ import React from "react"; import { Helpers, @@ -122,14 +123,24 @@ export const useVictoryCursorContainer = ( const newElements: React.ReactElement[] = []; const padding = getPadding(); - const cursorCoordinates = { - x: horizontal ? scale?.y?.(cursorValue.y) : scale?.x?.(cursorValue.x), - y: horizontal ? scale?.x?.(cursorValue.x) : scale?.y?.(cursorValue.y), - }; + const cursorCoordinates = + scale && + "x" in scale && + "y" in scale && + typeof scale.y === "function" && + typeof scale.x === "function" + ? { + x: horizontal ? scale.y(cursorValue.y) : scale.x(cursorValue.x), + y: horizontal ? scale.x(cursorValue.x) : scale.y(cursorValue.y), + } + : { + x: cursorValue.x, + y: cursorValue.y, + }; if (cursorLabel) { let labelProps = defaults({ active: true }, cursorLabelComponent.props, { - x: cursorCoordinates.x || 0 + cursorLabelOffset.x, - y: cursorCoordinates.y || 0 + cursorLabelOffset.y, + x: cursorCoordinates.x + cursorLabelOffset.x, + y: cursorCoordinates.y + cursorLabelOffset.y, datum: cursorValue, active: true, key: `${name}-cursor-label`, From d373b487918a931af06b8b1e3b402265b6fa8991 Mon Sep 17 00:00:00 2001 From: Kenan Date: Mon, 19 Feb 2024 11:36:01 +0000 Subject: [PATCH 30/36] fixed cursor overflow --- .../victory-cursor-container/src/victory-cursor-container.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/victory-cursor-container/src/victory-cursor-container.tsx b/packages/victory-cursor-container/src/victory-cursor-container.tsx index 208b0da9d..9115462d6 100644 --- a/packages/victory-cursor-container/src/victory-cursor-container.tsx +++ b/packages/victory-cursor-container/src/victory-cursor-container.tsx @@ -170,7 +170,7 @@ export const useVictoryCursorContainer = ( x1: cursorCoordinates.x, x2: cursorCoordinates.x, y1: padding.top, - y2: height || 0 - padding.bottom, + y2: (typeof height === "number" ? height : 0) - padding.bottom, style: cursorStyle, }), ); @@ -180,7 +180,7 @@ export const useVictoryCursorContainer = ( React.cloneElement(cursorComponent, { key: `${name}-y-cursor`, x1: padding.left, - x2: width || 0 - padding.right, + x2: (typeof width === "number" ? width : 0) - padding.right, y1: cursorCoordinates.y, y2: cursorCoordinates.y, style: cursorStyle, From 7488648b78ed42d0d3edad03cbd32a37bf100d52 Mon Sep 17 00:00:00 2001 From: Kenan Date: Mon, 19 Feb 2024 14:53:41 +0000 Subject: [PATCH 31/36] prevent scroll when zooming --- .../victory-core/src/victory-container/victory-container.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/victory-core/src/victory-container/victory-container.tsx b/packages/victory-core/src/victory-container/victory-container.tsx index be749d33e..8fb723e34 100644 --- a/packages/victory-core/src/victory-container/victory-container.tsx +++ b/packages/victory-core/src/victory-container/victory-container.tsx @@ -147,8 +147,7 @@ export const VictoryContainer = (initialProps: VictoryContainerProps) => { const handleWheel = (e: WheelEvent) => e.preventDefault(); - const container = (containerRef as React.RefObject) - ?.current; + const container = localContainerRef?.current; container?.addEventListener("wheel", handleWheel); return () => { From 7e1760314e05fe3eb26e1d076194c867e07d7d3e Mon Sep 17 00:00:00 2001 From: Kenan Date: Mon, 19 Feb 2024 14:53:50 +0000 Subject: [PATCH 32/36] remove comment --- .../victory-core/src/victory-container/victory-container.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/victory-core/src/victory-container/victory-container.tsx b/packages/victory-core/src/victory-container/victory-container.tsx index 8fb723e34..0e53148fe 100644 --- a/packages/victory-core/src/victory-container/victory-container.tsx +++ b/packages/victory-core/src/victory-container/victory-container.tsx @@ -142,7 +142,6 @@ export const VictoryContainer = (initialProps: VictoryContainerProps) => { } = useVictoryContainer(initialProps); React.useEffect(() => { - // TODO check that this works if (!events?.onWheel) return; const handleWheel = (e: WheelEvent) => e.preventDefault(); From 69f5108a85e1a4ce3a884198804366920311660f Mon Sep 17 00:00:00 2001 From: Kenan Date: Wed, 21 Feb 2024 08:17:57 +0000 Subject: [PATCH 33/36] add comments to merge refs utility --- .../victory-core/src/victory-util/merge-refs.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/victory-core/src/victory-util/merge-refs.ts b/packages/victory-core/src/victory-util/merge-refs.ts index 6c5a64067..ee08c87c2 100644 --- a/packages/victory-core/src/victory-util/merge-refs.ts +++ b/packages/victory-core/src/victory-util/merge-refs.ts @@ -1,11 +1,24 @@ +import * as Helpers from "./helpers"; + type Ref = React.MutableRefObject | React.LegacyRef | undefined | null; +/** + * Used to merge multiple React refs into a single callback ref. + * + * @example + * ```tsx + *
+ * ``` + */ export function mergeRefs(refs: Ref[]): React.RefCallback { return (value) => { refs.forEach((ref) => { - if (typeof ref === "function") { + // If the ref is a function, it's a callback ref and we call it with the value. + if (Helpers.isFunction(ref)) { ref(value); } else if (ref !== null && ref !== undefined) { + // If the ref is an object (not null and not undefined), it's an object ref. + // We assign the value to its 'current' property. (ref as React.MutableRefObject).current = value; } }); From 585d1eb71433abe31cb22337b7748fa202bd583a Mon Sep 17 00:00:00 2001 From: Kenan Date: Wed, 21 Feb 2024 08:18:07 +0000 Subject: [PATCH 34/36] rename container components const --- packages/victory-create-container/src/create-container.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/victory-create-container/src/create-container.tsx b/packages/victory-create-container/src/create-container.tsx index a5f6a6ee4..27dec0c95 100644 --- a/packages/victory-create-container/src/create-container.tsx +++ b/packages/victory-create-container/src/create-container.tsx @@ -98,7 +98,7 @@ const CONTAINER_HOOKS = { * - These containers are specific to the web. Victory Native has its own container components. * - For this utility, we only need the container components to extract the defaultEvents. */ -const CONTAINER_COMPONENTS = { +const CONTAINER_COMPONENTS_WEB = { zoom: VictoryZoomContainer, selection: VictorySelectionContainer, brush: VictoryBrushContainer, @@ -164,6 +164,6 @@ export function makeCreateContainerFunction< } export const createContainer = makeCreateContainerFunction( - CONTAINER_COMPONENTS, + CONTAINER_COMPONENTS_WEB, VictoryContainer, ); From ffc98414950478f45c40e61c001a2f713aeade1f Mon Sep 17 00:00:00 2001 From: Kenan Yusuf Date: Fri, 17 May 2024 14:32:45 +0100 Subject: [PATCH 35/36] refactor victory-portal to remove dependency on react-dom (#2870) --- packages/victory-core/src/exports.test.ts | 8 ++ packages/victory-core/src/index.ts | 3 +- .../victory-container/victory-container.tsx | 66 ++++++++-------- .../src/victory-portal/portal-context.ts | 15 ---- .../src/victory-portal/portal-context.tsx | 63 ++++++++++++++++ .../src/victory-portal/portal-outlet.tsx | 26 +++++++ .../src/victory-portal/victory-portal.tsx | 32 +++++--- .../src/components/victory-container.tsx | 75 +++++++++---------- .../victory-tooltip/src/victory-tooltip.tsx | 2 +- packages/victory/src/victory.test.ts | 3 + stories/victory-portal.stories.tsx | 57 ++++++++++++++ 11 files changed, 248 insertions(+), 102 deletions(-) delete mode 100644 packages/victory-core/src/victory-portal/portal-context.ts create mode 100644 packages/victory-core/src/victory-portal/portal-context.tsx create mode 100644 packages/victory-core/src/victory-portal/portal-outlet.tsx create mode 100644 stories/victory-portal.stories.tsx diff --git a/packages/victory-core/src/exports.test.ts b/packages/victory-core/src/exports.test.ts index e4f2f9065..4937f167c 100644 --- a/packages/victory-core/src/exports.test.ts +++ b/packages/victory-core/src/exports.test.ts @@ -66,6 +66,10 @@ import { Portal, PortalContext, PortalContextValue, + PortalOutlet, + PortalOutletProps, + PortalProvider, + PortalProviderProps, PortalProps, RangePropType, RangeTuple, @@ -137,6 +141,7 @@ import { Wrapper, addEvents, mergeRefs, + usePortalContext, } from "./index"; import { pick } from "lodash"; @@ -173,6 +178,8 @@ describe("victory-core", () => { "PointPathHelpers", "Portal", "PortalContext", + "PortalOutlet", + "PortalProvider", "Rect", "Scale", "Selection", @@ -196,6 +203,7 @@ describe("victory-core", () => { "Wrapper", "addEvents", "mergeRefs", + "usePortalContext", "useVictoryContainer", ] `); diff --git a/packages/victory-core/src/index.ts b/packages/victory-core/src/index.ts index f8980cddd..d9d4666b7 100644 --- a/packages/victory-core/src/index.ts +++ b/packages/victory-core/src/index.ts @@ -9,8 +9,9 @@ export * from "./victory-clip-container/victory-clip-container"; export * from "./victory-theme/types"; export * from "./victory-theme/victory-theme"; export * from "./victory-portal/portal"; -export * from "./victory-portal/portal-context"; export * from "./victory-portal/victory-portal"; +export * from "./victory-portal/portal-context"; +export * from "./victory-portal/portal-outlet"; export * from "./victory-primitives"; export { Border as Box } from "./victory-primitives"; export type { BorderProps as BoxProps } from "./victory-primitives"; diff --git a/packages/victory-core/src/victory-container/victory-container.tsx b/packages/victory-core/src/victory-container/victory-container.tsx index 0e53148fe..950ff86ca 100644 --- a/packages/victory-core/src/victory-container/victory-container.tsx +++ b/packages/victory-core/src/victory-container/victory-container.tsx @@ -1,12 +1,13 @@ import React, { useRef } from "react"; import { uniqueId } from "lodash"; import { Portal } from "../victory-portal/portal"; -import { PortalContext } from "../victory-portal/portal-context"; import * as UserProps from "../victory-util/user-props"; import { OriginType } from "../victory-label/victory-label"; import { D3Scale } from "../types/prop-types"; import { VictoryThemeDefinition } from "../victory-theme/types"; import { mergeRefs } from "../victory-util"; +import { PortalOutlet } from "../victory-portal/portal-outlet"; +import { PortalProvider } from "../victory-portal/portal-context"; export interface VictoryContainerProps { "aria-describedby"?: string; @@ -58,10 +59,6 @@ export function useVictoryContainer( const localContainerRef = useRef(null); - const [portalElement, setPortalElement] = React.useState(); - - const portalRef = (element: SVGSVGElement) => setPortalElement(element); - // Generated ID stored in ref because it needs to persist across renders const generatedId = useRef(uniqueId("victory-container-")); const containerId = props.containerId ?? generatedId.current; @@ -103,8 +100,6 @@ export function useVictoryContainer( ariaLabelledBy, ariaDescribedBy, userProps, - portalRef, - portalElement, localContainerRef, }; } @@ -135,8 +130,6 @@ export const VictoryContainer = (initialProps: VictoryContainerProps) => { userProps, titleId, descId, - portalElement, - portalRef, containerRef, localContainerRef, } = useVictoryContainer(initialProps); @@ -156,22 +149,22 @@ export const VictoryContainer = (initialProps: VictoryContainerProps) => { }, []); return ( - -
+
+ { left: 0, }} > - {React.cloneElement(portalComponent, { - width, - height, - viewBox, - preserveAspectRatio, - style: { ...dimensions, overflow: "visible" }, - ref: portalRef, - })} +
-
-
+ +
); }; diff --git a/packages/victory-core/src/victory-portal/portal-context.ts b/packages/victory-core/src/victory-portal/portal-context.ts deleted file mode 100644 index 3551dcaf4..000000000 --- a/packages/victory-core/src/victory-portal/portal-context.ts +++ /dev/null @@ -1,15 +0,0 @@ -import React from "react"; - -export interface PortalContextValue { - portalElement: SVGSVGElement | undefined; -} - -/** - * The React context object consumers may use to access the context of the - * portal. - */ -export const PortalContext = React.createContext< - PortalContextValue | undefined ->(undefined); - -PortalContext.displayName = "PortalContext"; diff --git a/packages/victory-core/src/victory-portal/portal-context.tsx b/packages/victory-core/src/victory-portal/portal-context.tsx new file mode 100644 index 000000000..1ac6a95cc --- /dev/null +++ b/packages/victory-core/src/victory-portal/portal-context.tsx @@ -0,0 +1,63 @@ +import React from "react"; + +export interface PortalContextValue { + addChild: (id: string, node: React.ReactElement) => void; + removeChild: (id: string) => void; + children: Map; +} + +export const PortalContext = React.createContext< + PortalContextValue | undefined +>(undefined); +PortalContext.displayName = "PortalContext"; + +export const usePortalContext = () => { + const context = React.useContext(PortalContext); + return context; +}; + +export interface PortalProviderProps { + children?: React.ReactNode; +} + +export const PortalProvider = ({ children }: PortalProviderProps) => { + const [portalChildren, setPortalChildren] = React.useState< + Map + >(new Map()); + const addChild = React.useCallback( + (id: string, element: React.ReactElement) => { + setPortalChildren((prevChildren) => { + const nextChildren = new Map(prevChildren); + nextChildren.set(id, element); + return nextChildren; + }); + }, + [setPortalChildren], + ); + + const removeChild = React.useCallback( + (id: string) => { + setPortalChildren((prevChildren) => { + const nextChildren = new Map(prevChildren); + nextChildren.delete(id); + return nextChildren; + }); + }, + [setPortalChildren], + ); + + const contextValue: PortalContextValue = React.useMemo( + () => ({ + addChild, + removeChild, + children: portalChildren, + }), + [addChild, removeChild, portalChildren], + ); + + return ( + + {children} + + ); +}; diff --git a/packages/victory-core/src/victory-portal/portal-outlet.tsx b/packages/victory-core/src/victory-portal/portal-outlet.tsx new file mode 100644 index 000000000..0f8980757 --- /dev/null +++ b/packages/victory-core/src/victory-portal/portal-outlet.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { usePortalContext } from "./portal-context"; + +export interface PortalOutletProps { + as: React.ReactElement; + width?: number; + height?: number; + viewBox?: string; + preserveAspectRatio?: string; + style?: React.CSSProperties; + children?: (children: React.ReactElement[]) => React.ReactNode | undefined; +} + +export const PortalOutlet = ({ + as: portalComponent, + ...props +}: PortalOutletProps) => { + const portalContext = usePortalContext(); + + if (!portalContext) { + return null; + } + + const children = Array.from(portalContext.children.values()); + return React.cloneElement(portalComponent, props, children); +}; diff --git a/packages/victory-core/src/victory-portal/victory-portal.tsx b/packages/victory-core/src/victory-portal/victory-portal.tsx index b6f57ae5d..93bb4d9e3 100644 --- a/packages/victory-core/src/victory-portal/victory-portal.tsx +++ b/packages/victory-core/src/victory-portal/victory-portal.tsx @@ -2,21 +2,21 @@ import React from "react"; import { defaults } from "lodash"; import * as Log from "../victory-util/log"; import * as Helpers from "../victory-util/helpers"; -import { PortalContext } from "./portal-context"; -import { createPortal } from "react-dom"; +import { usePortalContext } from "./portal-context"; export interface VictoryPortalProps { - children?: React.ReactElement; + children: React.ReactElement; groupComponent?: React.ReactElement; } -const defaultProps: VictoryPortalProps = { +const defaultProps: Partial = { groupComponent: , }; export const VictoryPortal = (initialProps: VictoryPortalProps) => { const props = { ...defaultProps, ...initialProps }; - const portalContext = React.useContext(PortalContext); + const id = React.useId(); + const portalContext = usePortalContext(); if (!portalContext) { const msg = @@ -25,9 +25,9 @@ export const VictoryPortal = (initialProps: VictoryPortalProps) => { Log.warn(msg); } - const children = ( - Array.isArray(props.children) ? props.children[0] : props.children - ) as React.ReactElement; + const children = Array.isArray(props.children) + ? props.children[0] + : props.children; const { groupComponent } = props; const childProps = (children && children.props) || {}; const standardProps = childProps.groupComponent @@ -37,13 +37,21 @@ export const VictoryPortal = (initialProps: VictoryPortalProps) => { standardProps, childProps, Helpers.omit(props, ["children", "groupComponent"]), + { key: childProps.key ?? id }, ); const child = children && React.cloneElement(children, newProps); - // If there is no portal context, render the child in place - return portalContext?.portalElement - ? createPortal(child, portalContext.portalElement) - : child; + React.useEffect(() => { + portalContext?.addChild(id, child); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.children]); + + React.useEffect(() => { + return () => portalContext?.removeChild(id); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return portalContext ? null : child; }; VictoryPortal.role = "portal"; diff --git a/packages/victory-native/src/components/victory-container.tsx b/packages/victory-native/src/components/victory-container.tsx index d63763013..91e2a28b0 100644 --- a/packages/victory-native/src/components/victory-container.tsx +++ b/packages/victory-native/src/components/victory-container.tsx @@ -7,7 +7,8 @@ import { VictoryEventHandler, mergeRefs, useVictoryContainer, - PortalContext, + PortalProvider, + PortalOutlet, } from "victory-core/es"; import NativeHelpers from "../helpers/native-helpers"; import { Portal } from "./victory-portal/portal"; @@ -41,8 +42,6 @@ export const VictoryContainer = (initialProps: VictoryContainerNativeProps) => { viewBox, preserveAspectRatio, userProps, - portalRef, - portalElement, containerRef, events, onTouchStart, @@ -124,37 +123,37 @@ export const VictoryContainer = (initialProps: VictoryContainerNativeProps) => { const baseStyle = NativeHelpers.getStyle(style, ["width", "height"]); return ( - - + - - {/* The following Rect is a temporary solution until the following RNSVG issue is resolved https://github.com/react-native-svg/react-native-svg/issues/1488 */} - - {title ? {title} : null} - {desc ? {desc} : null} + {/* The following Rect is a temporary solution until the following RNSVG issue is resolved https://github.com/react-native-svg/react-native-svg/issues/1488 */} + + {title ? {title} : null} + {desc ? {desc} : null} + {children} { }} pointerEvents="box-none" > - } width={width} height={height} viewBox={viewBox} style={{ ...dimensions, overflow: "visible" }} - ref={portalRef} /> - - - + + + ); }; diff --git a/packages/victory-tooltip/src/victory-tooltip.tsx b/packages/victory-tooltip/src/victory-tooltip.tsx index b79e401e3..c851cda71 100644 --- a/packages/victory-tooltip/src/victory-tooltip.tsx +++ b/packages/victory-tooltip/src/victory-tooltip.tsx @@ -600,7 +600,7 @@ export class VictoryTooltip extends React.Component { const active = Helpers.evaluateProp(props.active, props); const { renderInPortal } = props; if (!active) { - return renderInPortal ? : null; + return null; } const evaluatedProps = this.getEvaluatedProps(props); const { flyoutComponent, labelComponent, groupComponent } = evaluatedProps; diff --git a/packages/victory/src/victory.test.ts b/packages/victory/src/victory.test.ts index 3ea13f275..91f25453e 100644 --- a/packages/victory/src/victory.test.ts +++ b/packages/victory/src/victory.test.ts @@ -336,6 +336,8 @@ describe("victory", () => { "PointPathHelpers", "Portal", "PortalContext", + "PortalOutlet", + "PortalProvider", "RawZoomHelpers", "Rect", "Scale", @@ -407,6 +409,7 @@ describe("victory", () => { "makeCreateContainerFunction", "mergeRefs", "useCanvasContext", + "usePortalContext", "useVictoryBrushContainer", "useVictoryContainer", "useVictoryCursorContainer", diff --git a/stories/victory-portal.stories.tsx b/stories/victory-portal.stories.tsx new file mode 100644 index 000000000..a763bfac1 --- /dev/null +++ b/stories/victory-portal.stories.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import { VictoryChart } from "../packages/victory-chart"; +import { VictoryBar } from "../packages/victory-bar"; +import { VictoryGroup } from "../packages/victory-group"; +import { VictoryLabel, VictoryPortal } from "../packages/victory-core"; +import { Meta } from "@storybook/react"; +import { storyContainer } from "./decorators"; + +const meta: Meta = { + title: "Victory Charts/SVG Container/VictoryPortal", + component: VictoryPortal, + tags: ["autodocs"], + decorators: [storyContainer], +}; + +export default meta; + +export const Default = () => { + return ( +
+ + + + + + } + data={[ + { x: 1, y: 1 }, + { x: 2, y: 2 }, + { x: 3, y: 5 }, + ]} + /> + + + + +
+ ); +}; From b82bc5b04f7b1b47d96546450560b98f86e41e7e Mon Sep 17 00:00:00 2001 From: Charlie Brown Date: Wed, 29 May 2024 14:01:02 -0500 Subject: [PATCH 36/36] Add minor changeset --- .changeset/giant-carrots-own.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/.changeset/giant-carrots-own.md b/.changeset/giant-carrots-own.md index be1c7a6a8..0305b8a3f 100644 --- a/.changeset/giant-carrots-own.md +++ b/.changeset/giant-carrots-own.md @@ -1,13 +1,14 @@ --- -"victory": patch -"victory-brush-container": patch -"victory-core": patch -"victory-create-container": patch -"victory-cursor-container": patch -"victory-native": patch -"victory-selection-container": patch -"victory-voronoi-container": patch -"victory-zoom-container": patch +"victory": minor +"victory-brush-container": minor +"victory-core": minor +"victory-create-container": minor +"victory-cursor-container": minor +"victory-native": minor +"victory-selection-container": minor +"victory-tooltip": minor +"victory-voronoi-container": minor +"victory-zoom-container": minor --- Refactor containers and portal to function components