Skip to content

Instantly share code, notes, and snippets.

@nitedani
Created October 26, 2023 19:22
Show Gist options
  • Save nitedani/7dcc7041daf48750e2f1849ac6a7e9ac to your computer and use it in GitHub Desktop.
Save nitedani/7dcc7041daf48750e2f1849ac6a7e9ac to your computer and use it in GitHub Desktop.
Discord-like floater window
import { motion, useSpring } from "framer-motion";
import {
MouseEventHandler,
ReactEventHandler,
useCallback,
useEffect,
useRef,
useState,
} from "react";
function convertRemToPixels(rem) {
return rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
}
export const Floater = ({
onDoubleClick,
children,
width = 300,
height,
deadzone = 5,
}: {
onDoubleClick?: () => void;
children: React.ReactNode;
width?: number;
height?: number;
deadzone?: number;
}) => {
const [, rerender] = useState({});
const motionRef = useRef<HTMLDivElement>(null);
const [mounted, setMounted] = useState(false);
const opacity = useSpring(0, { duration: 300 });
const margin = convertRemToPixels(1.5);
const xy = useRef({
x: 0,
y: 0,
});
const snapToCorner = useCallback(() => {
if (!motionRef.current) return;
const appRect = document.body.getBoundingClientRect();
const pipRect = motionRef.current.getBoundingClientRect();
const pipMiddleX = pipRect.width / 2;
const pipMiddleY = pipRect.height / 2;
if (xy.current.x + pipMiddleX > appRect.width / 2) {
xy.current.x = appRect.width - pipRect.width - margin;
} else {
xy.current.x = margin;
}
if (xy.current.y + pipMiddleY > appRect.height / 2) {
xy.current.y = appRect.height - pipRect.height - margin;
} else {
xy.current.y = margin;
}
rerender({});
}, [margin]);
const onResize: ReactEventHandler<HTMLDivElement> = (e) => {
const appRect = document.body.getBoundingClientRect();
const pipRect = e.currentTarget.getBoundingClientRect();
if (!pipRect.width || !pipRect.height) {
return;
}
// bottom right corner
xy.current.x = appRect.width - pipRect.width - margin;
xy.current.y = appRect.height - pipRect.height - margin;
setMounted(true);
opacity.set(1);
};
useEffect(() => {
const listener = (e) => {
snapToCorner();
};
window.addEventListener("resize", listener);
window.addEventListener("orientationchange", listener);
window.addEventListener("blur", listener);
return () => {
window.removeEventListener("resize", listener);
window.removeEventListener("orientationchange", listener);
window.removeEventListener("blur", listener);
};
}, []);
const onMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
e.preventDefault();
e.stopPropagation();
const clickedPos = { x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY };
const listener = () => {
snapToCorner();
window.removeEventListener("mouseup", listener);
window.removeEventListener("mousemove", listener2);
};
window.addEventListener("mouseup", listener);
const listener2 = (e: MouseEvent) => {
if (
Math.abs(clickedPos.x - e.offsetX) < deadzone &&
Math.abs(clickedPos.y - e.offsetY) < deadzone
) {
return;
}
let correctedX = e.x - clickedPos.x;
let correctedY = e.y - clickedPos.y;
const appRect = document.body.getBoundingClientRect();
const pipRect = motionRef.current!.getBoundingClientRect();
if (correctedX < 0) {
correctedX = 0;
}
if (correctedX > appRect.width - pipRect.width) {
correctedX = appRect.width - pipRect.width;
}
if (correctedY < 0) {
correctedY = 0;
}
if (correctedY > appRect.height - pipRect.height) {
correctedY = appRect.height - pipRect.height;
}
xy.current = { x: correctedX, y: correctedY };
rerender({});
};
window.addEventListener("mousemove", listener2);
};
const pip = (
<div
ref={motionRef}
onResize={onResize}
style={{
width,
}}
>
{children}
</div>
);
// calculate initial size and position in a hidden div to avoid flicker
if (!mounted) {
return (
<div
style={{
position: "fixed",
opacity: 0,
}}
>
{pip}
</div>
);
}
return (
<motion.div
role="group"
onMouseDown={onMouseDown}
onDoubleClick={onDoubleClick}
animate={{ x: xy.current.x, y: xy.current.y }}
style={{
opacity,
position: "absolute",
width: "300px",
zIndex: 100,
boxShadow: "var(--chakra-shadows-lg)",
borderRadius: "var(--chakra-radii-xl)",
overflow: "hidden",
cursor: "grab",
}}
transition={{
type: "spring",
damping: 20,
stiffness: 250,
restDelta: 0.001,
}}
initial={{
x: xy.current.x,
y: xy.current.y,
}}
>
{pip}
</motion.div>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment