Skip to content

Commit 6c00505

Browse files
committed
Rewrite victory-container as a function component
1 parent 3dee0b6 commit 6c00505

File tree

5 files changed

+203
-0
lines changed

5 files changed

+203
-0
lines changed

packages/victory-core/src/exports.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ import {
135135
WhiskerProps,
136136
Wrapper,
137137
addEvents,
138+
mergeRefs,
138139
} from "victory-core";
139140
import { pick } from "lodash";
140141

@@ -193,6 +194,7 @@ describe("victory-core", () => {
193194
"Whisker",
194195
"Wrapper",
195196
"addEvents",
197+
"mergeRefs",
196198
]
197199
`);
198200
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import React, { useRef } from "react";
2+
import { uniqueId } from "lodash";
3+
import { Portal } from "../victory-portal/portal";
4+
import { PortalContext } from "../victory-portal/portal-context";
5+
import TimerContext from "../victory-util/timer-context";
6+
import * as UserProps from "../victory-util/user-props";
7+
import { OriginType } from "../victory-label/victory-label";
8+
import { D3Scale } from "../types/prop-types";
9+
import { VictoryThemeDefinition } from "../victory-theme/types";
10+
import { mergeRefs } from "../victory-util";
11+
12+
export interface VictoryContainerProps {
13+
"aria-describedby"?: string;
14+
"aria-labelledby"?: string;
15+
children?: React.ReactElement | React.ReactElement[];
16+
className?: string;
17+
containerId?: number | string;
18+
containerRef?: React.Ref<HTMLElement>;
19+
desc?: string;
20+
events?: React.DOMAttributes<any>;
21+
height?: number;
22+
name?: string;
23+
origin?: OriginType;
24+
polar?: boolean;
25+
portalComponent?: React.ReactElement;
26+
portalZIndex?: number;
27+
preserveAspectRatio?: string;
28+
responsive?: boolean;
29+
role?: string;
30+
scale?: {
31+
x?: D3Scale;
32+
y?: D3Scale;
33+
};
34+
style?: React.CSSProperties;
35+
tabIndex?: number;
36+
theme?: VictoryThemeDefinition;
37+
title?: string;
38+
width?: number;
39+
// Props defined by the Open UI Automation (OUIA) 1.0-RC spec
40+
// See https://ouia.readthedocs.io/en/latest/README.html#ouia-component
41+
ouiaId?: number | string;
42+
ouiaSafe?: boolean;
43+
ouiaType?: string;
44+
}
45+
46+
const defaultProps = {
47+
className: "VictoryContainer",
48+
portalComponent: <Portal />,
49+
portalZIndex: 99,
50+
responsive: true,
51+
role: "img",
52+
};
53+
54+
export const VictoryContainerFn = (initialProps: VictoryContainerProps) => {
55+
const propsWithDefaults = { ...defaultProps, ...initialProps };
56+
const {
57+
role,
58+
title,
59+
desc,
60+
children,
61+
className,
62+
portalZIndex,
63+
portalComponent,
64+
width,
65+
height,
66+
style,
67+
tabIndex,
68+
responsive,
69+
events,
70+
ouiaId,
71+
ouiaSafe,
72+
ouiaType,
73+
} = propsWithDefaults;
74+
75+
const containerRef = useRef<HTMLDivElement>(null);
76+
77+
const portalRef = useRef<Portal>(null);
78+
79+
// Generated ID stored in ref because it needs to persist across renders
80+
const generatedId = useRef(uniqueId("victory-container-"));
81+
const containerId = propsWithDefaults.containerId ?? generatedId;
82+
83+
const getIdForElement = (elName: string) => `${containerId}-${elName}`;
84+
85+
const userProps = UserProps.getSafeUserProps(propsWithDefaults);
86+
87+
const dimensions = responsive
88+
? { width: "100%", height: "100%" }
89+
: { width, height };
90+
91+
const viewBox = responsive ? `0 0 ${width} ${height}` : undefined;
92+
93+
const preserveAspectRatio = responsive
94+
? propsWithDefaults.preserveAspectRatio
95+
: undefined;
96+
97+
const ariaLabelledBy =
98+
[title && getIdForElement("title"), propsWithDefaults["aria-labelledby"]]
99+
.filter(Boolean)
100+
.join(" ") || undefined;
101+
102+
const ariaDescribedBy =
103+
[desc && getIdForElement("desc"), propsWithDefaults["aria-describedby"]]
104+
.filter(Boolean)
105+
.join(" ") || undefined;
106+
107+
const handleWheel = (e: WheelEvent) => e.preventDefault();
108+
109+
React.useEffect(() => {
110+
// TODO check that this works
111+
if (!propsWithDefaults.events?.onWheel) return;
112+
113+
const container = containerRef?.current;
114+
container?.addEventListener("wheel", handleWheel);
115+
116+
return () => {
117+
container?.removeEventListener("wheel", handleWheel);
118+
};
119+
}, []);
120+
121+
return (
122+
<PortalContext.Provider
123+
value={{
124+
portalUpdate: portalRef.current?.portalUpdate as any,
125+
portalRegister: portalRef.current?.portalRegister as any,
126+
portalDeregister: portalRef.current?.portalDeregister as any,
127+
}}
128+
>
129+
<div
130+
className={className}
131+
style={{
132+
...style,
133+
width: responsive ? style?.width : dimensions.width,
134+
height: responsive ? style?.height : dimensions.height,
135+
pointerEvents: "none",
136+
touchAction: "none",
137+
position: "relative",
138+
}}
139+
data-ouia-component-id={ouiaId}
140+
data-ouia-component-type={ouiaType}
141+
data-ouia-safe={ouiaSafe}
142+
ref={mergeRefs([containerRef, propsWithDefaults.containerRef])}
143+
>
144+
<svg
145+
width={width}
146+
height={height}
147+
tabIndex={tabIndex}
148+
role={role}
149+
aria-labelledby={ariaLabelledBy}
150+
aria-describedby={ariaDescribedBy}
151+
viewBox={viewBox}
152+
preserveAspectRatio={preserveAspectRatio}
153+
style={{ ...dimensions, pointerEvents: "all" }}
154+
{...userProps}
155+
{...events}
156+
>
157+
{title ? <title id={getIdForElement("title")}>{title}</title> : null}
158+
{desc ? <desc id={getIdForElement("desc")}>{desc}</desc> : null}
159+
{children}
160+
</svg>
161+
<div
162+
style={{
163+
...dimensions,
164+
zIndex: portalZIndex,
165+
position: "absolute",
166+
top: 0,
167+
left: 0,
168+
}}
169+
>
170+
{React.cloneElement(portalComponent, {
171+
width,
172+
height,
173+
viewBox,
174+
preserveAspectRatio,
175+
style: { ...dimensions, overflow: "visible" },
176+
ref: portalRef,
177+
})}
178+
</div>
179+
</div>
180+
</PortalContext.Provider>
181+
);
182+
};
183+
184+
VictoryContainerFn.role = "container";
185+
VictoryContainerFn.contextType = TimerContext;

packages/victory-core/src/victory-container/victory-container.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ export interface VictoryContainerProps {
4141
width?: number;
4242
}
4343

44+
export { VictoryContainerFn } from "./victory-container-fn";
45+
4446
export class VictoryContainer<
4547
TProps extends VictoryContainerProps,
4648
> extends React.Component<TProps> {

packages/victory-core/src/victory-util/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from "./add-events";
2+
export * from "./merge-refs";
23
export * as Axis from "./axis";
34
export * as Collection from "./collection";
45
export * from "./common-props";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
type Ref<T> = React.MutableRefObject<T> | React.LegacyRef<T> | undefined | null;
2+
3+
export function mergeRefs<T>(refs: Ref<T>[]): React.RefCallback<T> {
4+
return (value) => {
5+
refs.forEach((ref) => {
6+
if (typeof ref === "function") {
7+
ref(value);
8+
} else if (ref !== null && ref !== undefined) {
9+
(ref as React.MutableRefObject<T | null>).current = value;
10+
}
11+
});
12+
};
13+
}

0 commit comments

Comments
 (0)