fix: profile lifting
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m39s

This commit is contained in:
Hewston Fox
2026-03-22 13:25:49 +02:00
parent 5e9acffa09
commit e3088b7c47
20 changed files with 280 additions and 112 deletions

View File

@@ -1,14 +1,20 @@
import { createContext, useCallback, useContext, useRef, useState } from "react";
import type { ReactNode } from "react";
import { cloneWithoutAnimations } from "./Liftable";
type LiftContextValue = {
liftedIds: Set<string>;
alwaysLiftedIds: Set<string>;
modalOpen: boolean;
setLiftedIds: (ids: string[]) => void;
registerAlways: (id: string) => void;
unregisterAlways: (id: string) => void;
registerModal: () => void;
beginModalClose: () => void;
endModalClose: () => void;
portalContainer: HTMLElement | null;
setPortalContainer: (el: HTMLElement | null) => void;
setCloneContainer: (el: HTMLElement | null) => void;
};
const LiftContext = createContext<LiftContextValue | null>(null);
@@ -22,13 +28,41 @@ export function useLift(): LiftContextValue {
export function LiftProvider({ children }: { children: ReactNode }) {
const [liftedIds, setLiftedIdsRaw] = useState<Set<string>>(new Set());
const [alwaysLiftedIds, setAlwaysLiftedIds] = useState<Set<string>>(new Set());
const [modalOpen, setModalOpen] = useState(false);
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
const alwaysRef = useRef<Set<string>>(new Set());
const modalCountRef = useRef(0);
const cloneContainerRef = useRef<HTMLElement | null>(null);
const setLiftedIds = useCallback((ids: string[]) => {
setLiftedIdsRaw(new Set(ids));
}, []);
const registerModal = useCallback(() => {
modalCountRef.current++;
setModalOpen(true);
}, []);
const beginModalClose = useCallback(() => {
if (portalContainer && cloneContainerRef.current) {
cloneWithoutAnimations(portalContainer, cloneContainerRef.current);
}
modalCountRef.current--;
if (modalCountRef.current === 0) {
setModalOpen(false);
}
}, [portalContainer]);
const endModalClose = useCallback(() => {
if (cloneContainerRef.current) {
cloneContainerRef.current.innerHTML = "";
}
}, []);
const setCloneContainer = useCallback((el: HTMLElement | null) => {
cloneContainerRef.current = el;
}, []);
const registerAlways = useCallback((id: string) => {
alwaysRef.current.add(id);
setAlwaysLiftedIds(new Set(alwaysRef.current));
@@ -44,11 +78,16 @@ export function LiftProvider({ children }: { children: ReactNode }) {
value={{
liftedIds,
alwaysLiftedIds,
modalOpen,
setLiftedIds,
registerAlways,
unregisterAlways,
registerModal,
beginModalClose,
endModalClose,
portalContainer,
setPortalContainer,
setCloneContainer,
}}
>
{children}

View File

@@ -3,14 +3,26 @@ import { useLift } from "./LiftContext";
import classes from "./LiftLayer.module.css";
export function LiftLayer() {
const { setPortalContainer } = useLift();
const { setPortalContainer, setCloneContainer } = useLift();
const refCallback = useCallback(
const portalRef = useCallback(
(node: HTMLDivElement | null) => {
setPortalContainer(node);
},
[setPortalContainer],
);
return <div ref={refCallback} className={classes.liftLayer} />;
const cloneRef = useCallback(
(node: HTMLDivElement | null) => {
setCloneContainer(node);
},
[setCloneContainer],
);
return (
<>
<div ref={portalRef} className={classes.liftLayer} />
<div ref={cloneRef} className={classes.liftLayer} />
</>
);
}

View File

@@ -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)}
</>
);
}

View File

@@ -17,9 +17,18 @@ type Props = {
};
export default function Modal({ open, children, onClose, liftIds, title, className }: Props) {
const { setLiftedIds } = useLift();
const { setLiftedIds, registerModal, beginModalClose, endModalClose } = useLift();
const prevLiftIdsRef = useRef<string>("");
useEffect(() => {
if (open) {
registerModal();
return () => {
beginModalClose();
};
}
}, [open, registerModal, beginModalClose]);
useEffect(() => {
const key = liftIds?.join(",") ?? "";
@@ -45,7 +54,7 @@ export default function Modal({ open, children, onClose, liftIds, title, classNa
}, [setLiftedIds]);
return (
<AnimatePresence>
<AnimatePresence onExitComplete={endModalClose}>
{open && (
<>
<motion.div