Skip to content

Commit

Permalink
refactor victory-portal to remove dependency on react-dom (#2870)
Browse files Browse the repository at this point in the history
  • Loading branch information
KenanYusuf authored May 17, 2024
1 parent 3a4b911 commit ffc9841
Show file tree
Hide file tree
Showing 11 changed files with 248 additions and 102 deletions.
8 changes: 8 additions & 0 deletions packages/victory-core/src/exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ import {
Portal,
PortalContext,
PortalContextValue,
PortalOutlet,
PortalOutletProps,
PortalProvider,
PortalProviderProps,
PortalProps,
RangePropType,
RangeTuple,
Expand Down Expand Up @@ -137,6 +141,7 @@ import {
Wrapper,
addEvents,
mergeRefs,
usePortalContext,
} from "./index";
import { pick } from "lodash";

Expand Down Expand Up @@ -173,6 +178,8 @@ describe("victory-core", () => {
"PointPathHelpers",
"Portal",
"PortalContext",
"PortalOutlet",
"PortalProvider",
"Rect",
"Scale",
"Selection",
Expand All @@ -196,6 +203,7 @@ describe("victory-core", () => {
"Wrapper",
"addEvents",
"mergeRefs",
"usePortalContext",
"useVictoryContainer",
]
`);
Expand Down
3 changes: 2 additions & 1 deletion packages/victory-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
66 changes: 31 additions & 35 deletions packages/victory-core/src/victory-container/victory-container.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React, { useRef } from "react";
import { uniqueId } from "lodash";
import { Portal } from "../victory-portal/portal";
import { PortalContext } from "../victory-portal/portal-context";
import * as UserProps from "../victory-util/user-props";
import { OriginType } from "../victory-label/victory-label";
import { D3Scale } from "../types/prop-types";
import { VictoryThemeDefinition } from "../victory-theme/types";
import { mergeRefs } from "../victory-util";
import { PortalOutlet } from "../victory-portal/portal-outlet";
import { PortalProvider } from "../victory-portal/portal-context";

export interface VictoryContainerProps {
"aria-describedby"?: string;
Expand Down Expand Up @@ -58,10 +59,6 @@ export function useVictoryContainer<TProps extends VictoryContainerProps>(

const localContainerRef = useRef<HTMLDivElement>(null);

const [portalElement, setPortalElement] = React.useState<SVGSVGElement>();

const portalRef = (element: SVGSVGElement) => setPortalElement(element);

// Generated ID stored in ref because it needs to persist across renders
const generatedId = useRef(uniqueId("victory-container-"));
const containerId = props.containerId ?? generatedId.current;
Expand Down Expand Up @@ -103,8 +100,6 @@ export function useVictoryContainer<TProps extends VictoryContainerProps>(
ariaLabelledBy,
ariaDescribedBy,
userProps,
portalRef,
portalElement,
localContainerRef,
};
}
Expand Down Expand Up @@ -135,8 +130,6 @@ export const VictoryContainer = (initialProps: VictoryContainerProps) => {
userProps,
titleId,
descId,
portalElement,
portalRef,
containerRef,
localContainerRef,
} = useVictoryContainer(initialProps);
Expand All @@ -156,22 +149,22 @@ export const VictoryContainer = (initialProps: VictoryContainerProps) => {
}, []);

return (
<PortalContext.Provider value={{ portalElement }}>
<div
className={className}
style={{
...style,
width: responsive ? style?.width : dimensions.width,
height: responsive ? style?.height : dimensions.height,
pointerEvents: "none",
touchAction: "none",
position: "relative",
}}
data-ouia-component-id={ouiaId}
data-ouia-component-type={ouiaType}
data-ouia-safe={ouiaSafe}
ref={mergeRefs([localContainerRef, containerRef])}
>
<div
className={className}
style={{
...style,
width: responsive ? style?.width : dimensions.width,
height: responsive ? style?.height : dimensions.height,
pointerEvents: "none",
touchAction: "none",
position: "relative",
}}
data-ouia-component-id={ouiaId}
data-ouia-component-type={ouiaType}
data-ouia-safe={ouiaSafe}
ref={mergeRefs([localContainerRef, containerRef])}
>
<PortalProvider>
<svg
width={width}
height={height}
Expand All @@ -198,17 +191,20 @@ export const VictoryContainer = (initialProps: VictoryContainerProps) => {
left: 0,
}}
>
{React.cloneElement(portalComponent, {
width,
height,
viewBox,
preserveAspectRatio,
style: { ...dimensions, overflow: "visible" },
ref: portalRef,
})}
<PortalOutlet
as={portalComponent}
width={width}
height={height}
viewBox={viewBox}
preserveAspectRatio={preserveAspectRatio}
style={{
...dimensions,
overflow: "visible",
}}
/>
</div>
</div>
</PortalContext.Provider>
</PortalProvider>
</div>
);
};

Expand Down
15 changes: 0 additions & 15 deletions packages/victory-core/src/victory-portal/portal-context.ts

This file was deleted.

63 changes: 63 additions & 0 deletions packages/victory-core/src/victory-portal/portal-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from "react";

export interface PortalContextValue {
addChild: (id: string, node: React.ReactElement) => void;
removeChild: (id: string) => void;
children: Map<string, React.ReactElement>;
}

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<string, React.ReactElement>
>(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 (
<PortalContext.Provider value={contextValue}>
{children}
</PortalContext.Provider>
);
};
26 changes: 26 additions & 0 deletions packages/victory-core/src/victory-portal/portal-outlet.tsx
Original file line number Diff line number Diff line change
@@ -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);
};
32 changes: 20 additions & 12 deletions packages/victory-core/src/victory-portal/victory-portal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@ import React from "react";
import { defaults } from "lodash";
import * as Log from "../victory-util/log";
import * as Helpers from "../victory-util/helpers";
import { PortalContext } from "./portal-context";
import { createPortal } from "react-dom";
import { usePortalContext } from "./portal-context";

export interface VictoryPortalProps {
children?: React.ReactElement;
children: React.ReactElement;
groupComponent?: React.ReactElement;
}

const defaultProps: VictoryPortalProps = {
const defaultProps: Partial<VictoryPortalProps> = {
groupComponent: <g />,
};

export const VictoryPortal = (initialProps: VictoryPortalProps) => {
const props = { ...defaultProps, ...initialProps };
const portalContext = React.useContext(PortalContext);
const id = React.useId();
const portalContext = usePortalContext();

if (!portalContext) {
const msg =
Expand All @@ -25,9 +25,9 @@ export const VictoryPortal = (initialProps: VictoryPortalProps) => {
Log.warn(msg);
}

const children = (
Array.isArray(props.children) ? props.children[0] : props.children
) as React.ReactElement;
const children = Array.isArray(props.children)
? props.children[0]
: props.children;
const { groupComponent } = props;
const childProps = (children && children.props) || {};
const standardProps = childProps.groupComponent
Expand All @@ -37,13 +37,21 @@ export const VictoryPortal = (initialProps: VictoryPortalProps) => {
standardProps,
childProps,
Helpers.omit(props, ["children", "groupComponent"]),
{ key: childProps.key ?? id },
);
const child = children && React.cloneElement(children, newProps);

// If there is no portal context, render the child in place
return portalContext?.portalElement
? createPortal(child, portalContext.portalElement)
: child;
React.useEffect(() => {
portalContext?.addChild(id, child);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.children]);

React.useEffect(() => {
return () => portalContext?.removeChild(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return portalContext ? null : child;
};

VictoryPortal.role = "portal";
Loading

0 comments on commit ffc9841

Please sign in to comment.