diff --git a/.changeset/giant-carrots-own.md b/.changeset/giant-carrots-own.md new file mode 100644 index 000000000..0305b8a3f --- /dev/null +++ b/.changeset/giant-carrots-own.md @@ -0,0 +1,14 @@ +--- +"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 diff --git a/packages/victory-brush-container/src/victory-brush-container.tsx b/packages/victory-brush-container/src/victory-brush-container.tsx index 8b580b719..344577671 100644 --- a/packages/victory-brush-container/src/victory-brush-container.tsx +++ b/packages/victory-brush-container/src/victory-brush-container.tsx @@ -1,10 +1,11 @@ import React from "react"; import { - VictoryContainer, Selection, Rect, DomainTuple, VictoryContainerProps, + VictoryContainer, + VictoryEventHandler, } from "victory-core"; import { BrushHelpers } from "./brush-helpers"; import { defaults } from "lodash"; @@ -37,186 +38,188 @@ export interface VictoryBrushContainerProps extends VictoryContainerProps { ) => void; } -type ComponentClass = { new (props: TProps): React.Component }; +interface VictoryBrushContainerMutatedProps extends VictoryBrushContainerProps { + domain: { x: DomainTuple; y: DomainTuple }; + currentDomain: { x: DomainTuple; y: DomainTuple } | undefined; + cachedBrushDomain: { x: DomainTuple; y: DomainTuple } | undefined; +} -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, - }; +export const VICTORY_BRUSH_CONTAINER_DEFAULT_PROPS = { + 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 = { + ...VICTORY_BRUSH_CONTAINER_DEFAULT_PROPS, + ...(initialProps as VictoryBrushContainerMutatedProps), + }; + 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 = { ...VICTORY_BRUSH_CONTAINER_DEFAULT_PROPS, ...initialProps }; + const createEventHandler = + ( + handler: VictoryEventHandler, + isDisabled?: (targetProps: any) => boolean, + ): VictoryEventHandler => + // 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-core/src/exports.test.ts b/packages/victory-core/src/exports.test.ts index 78bb94f72..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, @@ -108,6 +112,7 @@ import { VictoryCommonProps, VictoryCommonThemeProps, VictoryContainer, + useVictoryContainer, VictoryContainerProps, VictoryDatableProps, VictoryLabel, @@ -135,6 +140,8 @@ import { WhiskerProps, Wrapper, addEvents, + mergeRefs, + usePortalContext, } from "./index"; import { pick } from "lodash"; @@ -171,6 +178,8 @@ describe("victory-core", () => { "PointPathHelpers", "Portal", "PortalContext", + "PortalOutlet", + "PortalProvider", "Rect", "Scale", "Selection", @@ -193,6 +202,9 @@ describe("victory-core", () => { "Whisker", "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/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-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-container/victory-container.tsx b/packages/victory-core/src/victory-container/victory-container.tsx index 16c47cef0..950ff86ca 100644 --- a/packages/victory-core/src/victory-container/victory-container.tsx +++ b/packages/victory-core/src/victory-container/victory-container.tsx @@ -1,13 +1,13 @@ -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"; +import { PortalOutlet } from "../victory-portal/portal-outlet"; +import { PortalProvider } from "../victory-portal/portal-context"; export interface VictoryContainerProps { "aria-describedby"?: string; @@ -21,9 +21,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,201 +36,176 @@ 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 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; +const defaultProps = { + className: "VictoryContainer", + portalComponent: , + portalZIndex: 99, + responsive: true, + role: "img", +}; + +export function useVictoryContainer( + initialProps: TProps, +) { + const props = { ...defaultProps, ...initialProps }; + const { title, desc, width, height, responsive } = props; + + const localContainerRef = 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.current; + + 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 titleId = getIdForElement("title"); + const descId = getIdForElement("desc"); + + return { + ...props, + titleId, + descId, + dimensions, + viewBox, + preserveAspectRatio, + ariaLabelledBy, + ariaDescribedBy, + userProps, + localContainerRef, }; - 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 }), +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, + containerRef, + localContainerRef, + } = useVictoryContainer(initialProps); + + React.useEffect(() => { + if (!events?.onWheel) return; + + const handleWheel = (e: WheelEvent) => e.preventDefault(); + + const container = localContainerRef?.current; + container?.addEventListener("wheel", handleWheel); + + return () => { + container?.removeEventListener("wheel", handleWheel); }; - } - - 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( - { + // 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([localContainerRef, containerRef])} + > + + + {title ? {title} : null} + {desc ? {desc} : null} + {children} +
- - {title ? ( - {title} - ) : null} - {desc ? ( - {desc} - ) : null} - {children} - -
- {React.cloneElement(portalComponent, { - ...portalProps, - ref: this.savePortalRef, - })} -
+
- - ); - } - - 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); - } -} +
+
+ ); +}; + +VictoryContainer.role = "container"; 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 5ebf3abe8..000000000 --- a/packages/victory-core/src/victory-portal/portal-context.ts +++ /dev/null @@ -1,14 +0,0 @@ -import React from "react"; - -export interface PortalContextValue { - portalRegister(): number; - portalUpdate(key: number, element: React.ReactElement): void; - portalDeregister(key: number): void; -} - -/** - * The React context object consumers may use to access the context of the - * portal. - */ -export const PortalContext = React.createContext({} as PortalContextValue); -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/portal.tsx b/packages/victory-core/src/victory-portal/portal.tsx index 50b3ed0d0..116e8acd4 100644 --- a/packages/victory-core/src/victory-portal/portal.tsx +++ b/packages/victory-core/src/victory-portal/portal.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { PortalContextValue } from "./portal-context"; export interface PortalProps { className?: string; @@ -9,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 Object.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..93bb4d9e3 100644 --- a/packages/victory-core/src/victory-portal/victory-portal.tsx +++ b/packages/victory-core/src/victory-portal/victory-portal.tsx @@ -2,85 +2,56 @@ 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 { usePortalContext } from "./portal-context"; export interface VictoryPortalProps { - children?: React.ReactElement; + 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: Partial = { + groupComponent: , +}; - componentWillUnmount() { - if (this.context && this.context.portalDeregister) { - this.context.portalDeregister(this.portalKey); - } - } +export const VictoryPortal = (initialProps: VictoryPortalProps) => { + const props = { ...defaultProps, ...initialProps }; + const id = React.useId(); + const portalContext = usePortalContext(); - // 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; + 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"]), + { key: childProps.key ?? id }, + ); + const child = children && React.cloneElement(children, newProps); + + 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-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..ee08c87c2 --- /dev/null +++ b/packages/victory-core/src/victory-util/merge-refs.ts @@ -0,0 +1,26 @@ +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 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; + } + }); + }; +} 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 7bb391cb2..000000000 --- a/packages/victory-create-container/src/create-container.ts +++ /dev/null @@ -1,173 +0,0 @@ -import React from "react"; -import { toPairs, groupBy, forOwn, 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 && !behaviors.includes(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.tsx b/packages/victory-create-container/src/create-container.tsx new file mode 100644 index 000000000..27dec0c95 --- /dev/null +++ b/packages/victory-create-container/src/create-container.tsx @@ -0,0 +1,169 @@ +import { + VictoryZoomContainer, + useVictoryZoomContainer, +} from "victory-zoom-container"; +import { + VictorySelectionContainer, + useVictorySelectionContainer, +} from "victory-selection-container"; +import React from "react"; +import { VictoryContainer } from "victory-core"; +import { forOwn, groupBy, isEmpty, toPairs } from "lodash"; +import { + VictoryVoronoiContainer, + useVictoryVoronoiContainer, +} from "victory-voronoi-container"; +import { + VictoryCursorContainer, + useVictoryCursorContainer, +} from "victory-cursor-container"; +import { + VictoryBrushContainer, + useVictoryBrushContainer, +} from "victory-brush-container"; + +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 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. + */ +const CONTAINER_HOOKS = { + zoom: useVictoryZoomContainer, + selection: useVictorySelectionContainer, + brush: useVictoryBrushContainer, + cursor: useVictoryCursorContainer, + voronoi: useVictoryVoronoiContainer, +}; + +/** + * 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_WEB = { + zoom: VictoryZoomContainer, + selection: VictorySelectionContainer, + brush: VictoryBrushContainer, + cursor: VictoryCursorContainer, + voronoi: VictoryVoronoiContainer, +}; + +type ContainerComponents = Record< + ContainerType, + React.ComponentType & { + defaultEvents: (props: any) => any[]; + } +>; + +export function makeCreateContainerFunction< + TContainerComponents extends ContainerComponents, +>( + containerComponents: TContainerComponents, + VictoryContainerBase: typeof VictoryContainer, +) { + type ContainerProps = React.ComponentProps< + TContainerComponents[T] + >; + + 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]; + + const CombinedContainer = ( + props: ContainerProps & ContainerProps, + ) => { + const { children: childrenA, props: propsA } = useContainerA(props); + const { children: combinedChildren, props: combinedProps } = + useContainerB({ + ...propsA, + children: childrenA, + }); + + return ( + + {combinedChildren} + + ); + }; + + CombinedContainer.displayName = `Victory${containerA}${containerB}Container`; + CombinedContainer.role = "container"; + CombinedContainer.defaultEvents = ( + props: ContainerProps & ContainerProps, + ) => + combineDefaultEvents([ + ...ContainerA.defaultEvents(props), + ...ContainerB.defaultEvents(props), + ]); + + return CombinedContainer; + }; +} + +export const createContainer = makeCreateContainerFunction( + CONTAINER_COMPONENTS_WEB, + VictoryContainer, +); diff --git a/packages/victory-cursor-container/src/victory-cursor-container.tsx b/packages/victory-cursor-container/src/victory-cursor-container.tsx index 229a0f3c3..9115462d6 100644 --- a/packages/victory-cursor-container/src/victory-cursor-container.tsx +++ b/packages/victory-cursor-container/src/victory-cursor-container.tsx @@ -1,6 +1,6 @@ +/* eslint-disable complexity */ import React from "react"; import { - VictoryContainer, Helpers, VictoryContainerProps, CoordinatesPropType, @@ -8,6 +8,10 @@ import { ValueOrAccessor, VictoryLabel, LineSegment, + VictoryContainer, + VictoryEventHandler, + DomainTuple, + PaddingProps, } from "victory-core"; import { defaults, isObject } from "lodash"; import { CursorHelpers } from "./cursor-helpers"; @@ -22,191 +26,208 @@ export interface VictoryCursorContainerProps extends VictoryContainerProps { cursorLabelOffset?: CursorCoordinatesPropType; defaultCursorValue?: CursorCoordinatesPropType; disable?: boolean; + horizontal?: boolean; + padding?: PaddingProps; onCursorChange?: ( value: CursorCoordinatesPropType, props: VictoryCursorContainerProps, ) => 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); - }, - }, - }, - ]; - } +interface VictoryCursorContainerMutatedProps + extends VictoryCursorContainerProps { + cursorValue: CoordinatesPropType | null; + domain: { x: DomainTuple; y: DomainTuple }; +} - getCursorPosition(props) { - const { cursorValue, defaultCursorValue, domain, cursorDimension } = - props; - if (cursorValue) { - return cursorValue; - } +export const VICTORY_CURSOR_CONTAINER_DEFAULT_PROPS = { + cursorLabelComponent: , + cursorLabelOffset: { + x: 5, + y: -10, + }, + cursorComponent: , +}; + +export const useVictoryCursorContainer = ( + initialProps: VictoryCursorContainerProps, +) => { + const props = { + ...VICTORY_CURSOR_CONTAINER_DEFAULT_PROPS, + ...(initialProps as VictoryCursorContainerMutatedProps), + }; + const { children } = props; - if (typeof defaultCursorValue === "number") { - return { - x: (domain.x[0] + domain.x[1]) / 2, - y: (domain.y[0] + domain.y[1]) / 2, - [cursorDimension]: defaultCursorValue, - }; - } + const getCursorPosition = () => { + const { cursorValue, defaultCursorValue, domain, cursorDimension } = props; + if (cursorValue) { + return cursorValue; + } - return defaultCursorValue; + if (typeof defaultCursorValue === "number") { + return { + 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 } : {}), + }; } - 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 = 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); + }; - 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 = + 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 + 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: (typeof height === "number" ? height : 0) - padding.bottom, + style: cursorStyle, + }), + ); + } + if (cursorDimension === "y" || cursorDimension === undefined) { + newElements.push( + React.cloneElement(cursorComponent, { + key: `${name}-y-cursor`, + x1: padding.left, + x2: (typeof width === "number" ? width : 0) - 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 = { ...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); + + return [ + { + target: "parent", + eventHandlers: { + onMouseLeave: createEventHandler(CursorHelpers.onMouseLeave), + onMouseMove: createEventHandler(CursorHelpers.onMouseMove), + onTouchMove: createEventHandler(CursorHelpers.onMouseMove), + }, + }, + ]; +}; diff --git a/packages/victory-native/src/components/victory-brush-container.tsx b/packages/victory-native/src/components/victory-brush-container.tsx index 5782d39f4..21357314b 100644 --- a/packages/victory-native/src/components/victory-brush-container.tsx +++ b/packages/victory-native/src/components/victory-brush-container.tsx @@ -1,11 +1,12 @@ +/* eslint-disable react/no-multi-comp */ import React from "react"; import { Rect } from "react-native-svg"; -import { flow } from "lodash"; +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"; @@ -13,18 +14,8 @@ import NativeHelpers from "../helpers/native-helpers"; 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 @@ -35,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 VictoryBrushContainer = ( + initialProps: VictoryBrushContainerNativeProps, +) => { + const props = useVictoryBrushContainer({ + ...initialProps, + brushComponent: initialProps.brushComponent ?? , + handleComponent: initialProps.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); - }, - }, - }, - ]; - } - }; -} +VictoryBrushContainer.role = "container"; -const combinedMixin: ( - base: React.ComponentClass, -) => React.ComponentClass = flow( - originalBrushMixin, - nativeBrushMixin, -); +VictoryBrushContainer.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-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..91e2a28b0 100644 --- a/packages/victory-native/src/components/victory-container.tsx +++ b/packages/victory-native/src/components/victory-container.tsx @@ -3,8 +3,12 @@ import Svg, { Rect } from "react-native-svg"; import { get } from "lodash"; import { View, PanResponder } from "react-native"; import { - VictoryContainer as VictoryContainerBase, VictoryContainerProps, + VictoryEventHandler, + mergeRefs, + useVictoryContainer, + PortalProvider, + PortalOutlet, } from "victory-core/es"; import NativeHelpers from "../helpers/native-helpers"; import { Portal } from "./victory-portal/portal"; @@ -14,163 +18,164 @@ 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 { - 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, + 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); + }; + + const handleResponderMove = (event) => { + const { touches } = event.nativeEvent; + if (touches && touches.length === 2) { + callOptionalEventCallback("onTouchPinch", event); + } else { + callOptionalEventCallback("onTouchMove", event); + } + }; - constructor(props) { - super(props); - this.panResponder = this.getResponder(); - } + const handleResponderEnd = (event) => { + if (onTouchEnd) { + onTouchEnd(event); + } + callOptionalEventCallback("onTouchEnd", event); + }; - getResponder() { + 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 ( - + - - {/* - 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} - - + + } + width={width} + height={height} + viewBox={viewBox} + style={{ ...dimensions, overflow: "visible" }} + /> - - - ); - } -} + + + + ); +}; + +VictoryContainer.role = "container"; diff --git a/packages/victory-native/src/components/victory-cursor-container.tsx b/packages/victory-native/src/components/victory-cursor-container.tsx index 74c930124..d7d4c4688 100644 --- a/packages/victory-native/src/components/victory-cursor-container.tsx +++ b/packages/victory-native/src/components/victory-cursor-container.tsx @@ -1,9 +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"; @@ -13,70 +13,43 @@ 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< - 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 VictoryCursorContainer = ( + 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, -); +VictoryCursorContainer.role = "container"; -export const cursorContainerMixin = (base: React.ComponentClass) => - combinedMixin(base); +VictoryCursorContainer.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), + }, + }, + ]; +}; 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 ( + } + /> + ); +}; diff --git a/packages/victory-native/src/components/victory-selection-container.tsx b/packages/victory-native/src/components/victory-selection-container.tsx index d8262cf53..4c0f0fcc7 100644 --- a/packages/victory-native/src/components/victory-selection-container.tsx +++ b/packages/victory-native/src/components/victory-selection-container.tsx @@ -1,11 +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"; @@ -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 @@ -35,61 +26,52 @@ 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 VictorySelectionContainer = ( + initialProps: VictorySelectionContainerNativeProps, +) => { + 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 ?? ( + + ), + }); + return ; +}; + +VictorySelectionContainer.role = "container"; - // 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); - }, - }, - }, - ]; - } +VictorySelectionContainer.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), + }, + }, + ]; +}; diff --git a/packages/victory-native/src/components/victory-voronoi-container.tsx b/packages/victory-native/src/components/victory-voronoi-container.tsx index cf4d105d7..2635fe2cf 100644 --- a/packages/victory-native/src/components/victory-voronoi-container.tsx +++ b/packages/victory-native/src/components/victory-voronoi-container.tsx @@ -1,11 +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"; @@ -13,81 +13,57 @@ 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< - 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, + }, + }, + ]; +}; diff --git a/packages/victory-native/src/components/victory-zoom-container.tsx b/packages/victory-native/src/components/victory-zoom-container.tsx index 695183345..e3aae6bd0 100644 --- a/packages/victory-native/src/components/victory-zoom-container.tsx +++ b/packages/victory-native/src/components/victory-zoom-container.tsx @@ -1,97 +1,56 @@ -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"; 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< - 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), + }, + }, + ]; +}; 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, ); diff --git a/packages/victory-selection-container/src/victory-selection-container.tsx b/packages/victory-selection-container/src/victory-selection-container.tsx index ef2e3ae88..d6cd2c1ec 100644 --- a/packages/victory-selection-container/src/victory-selection-container.tsx +++ b/packages/victory-selection-container/src/victory-selection-container.tsx @@ -4,6 +4,7 @@ import { Rect, VictoryContainer, VictoryContainerProps, + VictoryEventHandler, } from "victory-core"; import { SelectionHelpers } from "./selection-helpers"; @@ -31,92 +32,94 @@ export interface VictorySelectionContainerProps extends VictoryContainerProps { selectionStyle?: React.CSSProperties; } -type ComponentClass = { new (props: TProps): React.Component }; +export const VICTORY_SELECTION_CONTAINER_DEFAULT_PROPS = { + 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, - }, - }; +interface VictorySelectionContainerMutatedProps + extends VictorySelectionContainerProps { + x1: number; + x2: number; + y1: number; + y2: number; +} - 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); - }, - }, - }, - ]; - } +export const useVictorySelectionContainer = ( + initialProps: VictorySelectionContainerProps, +) => { + const props = { + ...VICTORY_SELECTION_CONTAINER_DEFAULT_PROPS, + ...(initialProps as VictorySelectionContainerMutatedProps), + }; - 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 { 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); - // Overrides method in VictoryContainer - getChildren(props: TProps) { - return [...React.Children.toArray(props.children), this.getRect(props)]; - } + const shouldRenderRect = y1 && y2 && x1 && x2; + + return { + props, + children: [ + children, + shouldRenderRect && + React.cloneElement(selectionComponent, { + key: `${name}-selection`, + x, + y, + width, + height, + style: selectionStyle, + }), + ] as React.ReactElement[], }; -} +}; -export const VictorySelectionContainer = - selectionContainerMixin(VictoryContainer); +export const VictorySelectionContainer = ( + initialProps: VictorySelectionContainerProps, +) => { + const { props, children } = useVictorySelectionContainer(initialProps); + return {children}; +}; + +VictorySelectionContainer.role = "container"; + +VictorySelectionContainer.defaultEvents = ( + initialProps: VictorySelectionContainerProps, +) => { + const props = { + ...VICTORY_SELECTION_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); + + 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-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-voronoi-container/src/victory-voronoi-container.tsx b/packages/victory-voronoi-container/src/victory-voronoi-container.tsx index 015cf4f0e..c17be8d13 100644 --- a/packages/victory-voronoi-container/src/victory-voronoi-container.tsx +++ b/packages/victory-voronoi-container/src/victory-voronoi-container.tsx @@ -3,20 +3,19 @@ import React from "react"; import { defaults, pick } from "lodash"; import { VictoryTooltip } from "victory-tooltip"; import { - VictoryContainer, Helpers, VictoryContainerProps, PaddingProps, + VictoryContainer, + VictoryEventHandler, } from "victory-core"; import { VoronoiHelpers } from "./voronoi-helpers"; -type ComponentClass = { new (props: TProps): React.Component }; - 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; @@ -25,220 +24,223 @@ export interface VictoryVoronoiContainerProps extends VictoryContainerProps { voronoiBlacklist?: (string | RegExp)[]; voronoiDimension?: "x" | "y"; voronoiPadding?: PaddingProps; + horizontal?: boolean; } -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, - }, - }, - ]; - } +interface VictoryVoronoiContainerMutatedProps + extends VictoryVoronoiContainerProps { + mousePosition: { x: number; y: number }; + activePoints: any[]; +} - getDimension(props) { - const { horizontal, voronoiDimension } = props; - if (!horizontal || !voronoiDimension) { - return voronoiDimension; - } - return voronoiDimension === "x" ? "y" : "x"; - } +export const VICTORY_VORONOI_CONTAINER_DEFAULT_PROPS = { + 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 = { + ...VICTORY_VORONOI_CONTAINER_DEFAULT_PROPS, + ...(initialProps as VictoryVoronoiContainerMutatedProps), + }; + const { children } = props; - getPoint(point) { - const whitelist = ["_x", "_x1", "_x0", "_y", "_y1", "_y0"]; - return pick(point, whitelist); + const getDimension = () => { + const { horizontal, voronoiDimension } = props; + if (!horizontal || !voronoiDimension) { + return voronoiDimension; } + return voronoiDimension === "x" ? "y" : "x"; + }; - 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 getLabelPosition = (labelProps, points) => { + const { mousePosition, mouseFollowTooltips } = props; + const voronoiDimension = getDimension(); + const point = getPoint(points[0]); + const basePosition = Helpers.scalePoint(props, point); - 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); - }, []); - } - - 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 = { ...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); + + 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/victory-zoom-container.tsx b/packages/victory-zoom-container/src/victory-zoom-container.tsx index 48b3d683e..f2f69b58b 100644 --- a/packages/victory-zoom-container/src/victory-zoom-container.tsx +++ b/packages/victory-zoom-container/src/victory-zoom-container.tsx @@ -1,19 +1,24 @@ import React from "react"; -import { defaults } from "lodash"; import { ZoomHelpers } from "./zoom-helpers"; import { - Data, - DomainTuple, - Helpers, - VictoryContainer, VictoryClipContainer, VictoryContainerProps, + DomainTuple, + VictoryContainer, + Data, + VictoryEventHandler, } from "victory-core"; +import { defaults } from "lodash"; const DEFAULT_DOWNSAMPLE = 150; export type ZoomDimensionType = "x" | "y"; +export type ZoomDomain = { + x: DomainTuple; + y: DomainTuple; +}; + export interface VictoryZoomContainerProps extends VictoryContainerProps { allowPan?: boolean; allowZoom?: boolean; @@ -22,92 +27,146 @@ 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; + horizontal?: boolean; +} + +interface VictoryZoomContainerMutatedProps extends VictoryZoomContainerProps { + domain: ZoomDomain; + originalDomain: ZoomDomain; + currentDomain: ZoomDomain; + cachedZoomDomain: ZoomDomain; + scale: any; + polar: boolean; + origin: { x: number; y: number }; } -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, +export const VICTORY_ZOOM_CONTAINER_DEFAULT_PROPS = { + clipContainerComponent: , + allowPan: true, + allowZoom: true, + zoomActive: false, +}; + +export const useVictoryZoomContainer = ( + initialProps: VictoryZoomContainerProps, +) => { + const props = { + ...VICTORY_ZOOM_CONTAINER_DEFAULT_PROPS, + ...(initialProps as VictoryZoomContainerMutatedProps), + }; + 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; } - clipDataComponents(children: React.ReactElement[], props) { - const { scale, clipContainerComponent, polar, origin, horizontal } = - props; + 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]); @@ -123,124 +182,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 = { ...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); + + 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/src/victory.test.ts b/packages/victory/src/victory.test.ts index bb0e16e03..91f25453e 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", () => { @@ -330,6 +336,8 @@ describe("victory", () => { "PointPathHelpers", "Portal", "PortalContext", + "PortalOutlet", + "PortalProvider", "RawZoomHelpers", "Rect", "Scale", @@ -344,6 +352,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 +395,7 @@ describe("victory", () => { "Wrapper", "ZoomHelpers", "addEvents", - "brushContainerMixin", - "combineContainerMixins", "createContainer", - "cursorContainerMixin", "getBarPath", "getBarPosition", "getBarWidth", @@ -397,10 +407,15 @@ describe("victory", () => { "getVerticalBarPath", "getVerticalPolarBarPath", "makeCreateContainerFunction", - "selectionContainerMixin", + "mergeRefs", "useCanvasContext", - "voronoiContainerMixin", - "zoomContainerMixin", + "usePortalContext", + "useVictoryBrushContainer", + "useVictoryContainer", + "useVictoryCursorContainer", + "useVictorySelectionContainer", + "useVictoryVoronoiContainer", + "useVictoryZoomContainer", ] `); }); 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 }, + ]} + /> + + + + +
+ ); +};