@empoleon/solid-measure
Version:
Compute measurements of SolidJS components
143 lines (123 loc) • 3.94 kB
text/typescript
import {
Component,
createSignal,
onMount,
onCleanup,
splitProps,
createMemo,
} from "solid-js";
// @ts-ignore - ResizeObserver polyfill
import { createResizeObserver } from "@solid-primitives/resize-observer";
import getTypes, { MeasureProps } from "./get-types";
import getContentRect, {
ContentRectCalculations,
MeasurementType,
} from "./get-content-rect";
import getWindowOf from "./get-window-of";
export interface ContentRectState {
entry: Partial<DOMRectReadOnly>;
client: any;
offset: any;
scroll: any;
bounds: any;
margin: any;
}
export interface WithContentRectChildrenProps {
measure: (entries?: ResizeObserverEntry[]) => void;
measureRef: (node: Element) => void;
contentRect: ContentRectState;
}
export interface WithContentRectProps extends MeasureProps {
innerRef?: ((el: Element) => void) | { current?: Element | null };
onResize?: (contentRect: ContentRectCalculations) => void;
}
function withContentRect<T extends WithContentRectProps>(
types?: MeasurementType[]
) {
return function <P extends T>(
WrappedComponent: Component<P & WithContentRectChildrenProps>
): Component<P> {
const MeasureComponent: Component<P> = (props: P) => {
const [local, others] = splitProps(props as P & WithContentRectProps, [
"innerRef",
"onResize",
]);
const [contentRect, setContentRect] = createSignal<ContentRectState>({
entry: {},
client: {},
offset: {},
scroll: {},
bounds: {},
margin: {},
});
let animationFrameID: number | null = null;
let node: Element | null = null;
let windowRef: Window | null = null;
// createResizeObserver from @solid-primitives/resize-observer handles cleanup automatically
const measure = (rect?: DOMRectReadOnly) => {
if (!node) return;
const contentRectCalc = getContentRect(
node,
types || getTypes(props as MeasureProps)
);
const newContentRect: ContentRectState = {
entry: rect || {},
client: contentRectCalc.client || {},
offset: contentRectCalc.offset || {},
scroll: contentRectCalc.scroll || {},
bounds: contentRectCalc.bounds || {},
margin: contentRectCalc.margin || {},
};
if (windowRef) {
animationFrameID = windowRef.requestAnimationFrame(() => {
setContentRect(newContentRect);
if (typeof local.onResize === "function") {
local.onResize(contentRectCalc);
}
});
} else {
setContentRect(newContentRect);
if (typeof local.onResize === "function") {
local.onResize(contentRectCalc);
}
}
};
const handleRef = (newNode: Element) => {
node = newNode;
windowRef = getWindowOf(node);
if (local.innerRef) {
if (typeof local.innerRef === "function") {
local.innerRef(node);
} else {
local.innerRef.current = node;
}
}
};
onMount(() => {
if (node) {
createResizeObserver(node, measure);
}
});
onCleanup(() => {
if (windowRef && animationFrameID) {
windowRef.cancelAnimationFrame(animationFrameID);
}
// createResizeObserver from @solid-primitives/resize-observer
// handles cleanup automatically when the component is disposed
});
// Create a memo to make the component props reactive
const componentProps = createMemo(
() =>
({
...others,
measureRef: handleRef,
measure,
contentRect: contentRect(),
} as P & WithContentRectChildrenProps)
);
return WrappedComponent(componentProps());
};
return MeasureComponent;
};
}
export default withContentRect;