fix: profile lifting
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m39s
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m39s
This commit is contained in:
@@ -1,8 +1,29 @@
|
||||
import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
||||
import { useEffect, useId, useLayoutEffect, useRef } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useLift } from "./LiftContext";
|
||||
|
||||
/** Recursively strip CSS animations and transitions from a cloned DOM tree */
|
||||
function stripAnimations(node: Node) {
|
||||
if (node instanceof HTMLElement) {
|
||||
node.style.animation = "none";
|
||||
node.style.transition = "none";
|
||||
node.getAnimations().forEach((a) => a.cancel());
|
||||
}
|
||||
for (const child of Array.from(node.childNodes)) {
|
||||
stripAnimations(child);
|
||||
}
|
||||
}
|
||||
|
||||
function cloneWithoutAnimations(source: HTMLElement, target: HTMLElement) {
|
||||
target.innerHTML = "";
|
||||
for (const child of Array.from(source.children)) {
|
||||
const clone = child.cloneNode(true) as HTMLElement;
|
||||
stripAnimations(clone);
|
||||
target.appendChild(clone);
|
||||
}
|
||||
}
|
||||
|
||||
type LiftableProps = {
|
||||
id?: string;
|
||||
always?: boolean;
|
||||
@@ -23,7 +44,7 @@ export function useLiftable<T extends Record<string, unknown> = Record<string, n
|
||||
...extraProps
|
||||
} = (options ?? {}) as LiftableProps & Record<string, unknown>;
|
||||
const id = idProp ?? autoId;
|
||||
const { liftedIds, alwaysLiftedIds, registerAlways, unregisterAlways } = useLift();
|
||||
const { liftedIds, alwaysLiftedIds, modalOpen, registerAlways, unregisterAlways } = useLift();
|
||||
|
||||
useEffect(() => {
|
||||
if (always) {
|
||||
@@ -32,7 +53,7 @@ export function useLiftable<T extends Record<string, unknown> = Record<string, n
|
||||
}
|
||||
}, [always, id, registerAlways, unregisterAlways]);
|
||||
|
||||
const isLifted = liftedIds.has(id) || alwaysLiftedIds.has(id);
|
||||
const isLifted = liftedIds.has(id) || (alwaysLiftedIds.has(id) && modalOpen);
|
||||
|
||||
const element = (
|
||||
<Liftable id={id}>{render({ isLifted, ...extraProps } as { isLifted: boolean } & T)}</Liftable>
|
||||
@@ -41,13 +62,24 @@ export function useLiftable<T extends Record<string, unknown> = Record<string, n
|
||||
return { id, element };
|
||||
}
|
||||
|
||||
export { cloneWithoutAnimations };
|
||||
|
||||
export function Liftable({ id: idProp, always, children }: Props) {
|
||||
const autoId = useId();
|
||||
const id = idProp ?? autoId;
|
||||
const { liftedIds, alwaysLiftedIds, registerAlways, unregisterAlways, portalContainer } =
|
||||
useLift();
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const [rect, setRect] = useState<DOMRect | null>(null);
|
||||
const {
|
||||
liftedIds,
|
||||
alwaysLiftedIds,
|
||||
modalOpen,
|
||||
registerAlways,
|
||||
unregisterAlways,
|
||||
portalContainer,
|
||||
} = useLift();
|
||||
|
||||
const childrenHostRef = useRef<HTMLDivElement>(null);
|
||||
const inlineSlotRef = useRef<HTMLDivElement>(null);
|
||||
const portalWrapperRef = useRef<HTMLDivElement>(null);
|
||||
const wasLiftedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (always) {
|
||||
@@ -56,29 +88,43 @@ export function Liftable({ id: idProp, always, children }: Props) {
|
||||
}
|
||||
}, [always, id, registerAlways, unregisterAlways]);
|
||||
|
||||
const isLifted = liftedIds.has(id) || alwaysLiftedIds.has(id);
|
||||
const isLifted = liftedIds.has(id) || (alwaysLiftedIds.has(id) && modalOpen);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isLifted && wrapperRef.current) {
|
||||
setRect(wrapperRef.current.getBoundingClientRect());
|
||||
}
|
||||
if (!isLifted) {
|
||||
setRect(null);
|
||||
const host = childrenHostRef.current;
|
||||
const inlineSlot = inlineSlotRef.current;
|
||||
const portalWrapper = portalWrapperRef.current;
|
||||
if (!host || !inlineSlot || !portalWrapper) return;
|
||||
|
||||
if (isLifted && !wasLiftedRef.current) {
|
||||
const rect = inlineSlot.getBoundingClientRect();
|
||||
cloneWithoutAnimations(host, inlineSlot);
|
||||
portalWrapper.appendChild(host);
|
||||
portalWrapper.style.cssText = `position:fixed;top:${rect.top}px;left:${rect.left}px;width:${rect.width}px;height:${rect.height}px;pointer-events:auto`;
|
||||
wasLiftedRef.current = true;
|
||||
} else if (!isLifted && wasLiftedRef.current) {
|
||||
inlineSlot.innerHTML = "";
|
||||
inlineSlot.appendChild(host);
|
||||
portalWrapper.style.cssText = "display:none";
|
||||
wasLiftedRef.current = false;
|
||||
}
|
||||
}, [isLifted]);
|
||||
|
||||
// Re-measure on resize while lifted
|
||||
useEffect(() => {
|
||||
if (!isLifted || !wrapperRef.current) return;
|
||||
if (!isLifted || !inlineSlotRef.current || !portalWrapperRef.current) return;
|
||||
const portalWrapper = portalWrapperRef.current;
|
||||
const inlineSlot = inlineSlotRef.current;
|
||||
|
||||
const measure = () => {
|
||||
if (wrapperRef.current) {
|
||||
setRect(wrapperRef.current.getBoundingClientRect());
|
||||
}
|
||||
const rect = inlineSlot.getBoundingClientRect();
|
||||
portalWrapper.style.top = `${rect.top}px`;
|
||||
portalWrapper.style.left = `${rect.left}px`;
|
||||
portalWrapper.style.width = `${rect.width}px`;
|
||||
portalWrapper.style.height = `${rect.height}px`;
|
||||
};
|
||||
|
||||
const observer = new ResizeObserver(measure);
|
||||
observer.observe(wrapperRef.current);
|
||||
observer.observe(inlineSlot);
|
||||
window.addEventListener("resize", measure);
|
||||
|
||||
return () => {
|
||||
@@ -87,35 +133,13 @@ export function Liftable({ id: idProp, always, children }: Props) {
|
||||
};
|
||||
}, [isLifted]);
|
||||
|
||||
// When lifted and we have measurements + portal target, render in portal
|
||||
if (isLifted && rect && portalContainer) {
|
||||
return (
|
||||
<>
|
||||
{/* Placeholder preserves layout space */}
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
style={{ width: rect.width, height: rect.height, visibility: "hidden" }}
|
||||
/>
|
||||
{/* Portal children above blur */}
|
||||
{createPortal(
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: rect.top,
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>,
|
||||
portalContainer,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Normal inline rendering
|
||||
return <div ref={wrapperRef}>{children}</div>;
|
||||
return (
|
||||
<>
|
||||
<div ref={inlineSlotRef}>
|
||||
<div ref={childrenHostRef}>{children}</div>
|
||||
</div>
|
||||
{portalContainer &&
|
||||
createPortal(<div ref={portalWrapperRef} style={{ display: "none" }} />, portalContainer)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user