feat(platform): checkpoint current implementation state
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, type CSSProperties } from "react";
|
||||
import { useEffect, useRef, useState, type CSSProperties } from "react";
|
||||
|
||||
type PopoverAnchor =
|
||||
| { kind: "point"; x: number; y: number }
|
||||
@@ -17,6 +17,8 @@ interface UseViewportPopoverOptions {
|
||||
offset?: number;
|
||||
viewportPadding?: number;
|
||||
ignoreElements?: Array<HTMLElement | null>;
|
||||
ignoreSelectors?: string[];
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
export function useViewportPopover({
|
||||
@@ -29,37 +31,23 @@ export function useViewportPopover({
|
||||
offset = 8,
|
||||
viewportPadding = 16,
|
||||
ignoreElements = [],
|
||||
ignoreSelectors = [],
|
||||
zIndex = 9998,
|
||||
}: UseViewportPopoverOptions) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const frameRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function handlePointerDown(event: MouseEvent) {
|
||||
const target = event.target as Node;
|
||||
if (ref.current?.contains(target)) {
|
||||
return;
|
||||
}
|
||||
if (ignoreElements.some((element) => element?.contains(target))) {
|
||||
return;
|
||||
}
|
||||
onClose();
|
||||
const computeStyle = (): CSSProperties => {
|
||||
if (typeof window === "undefined") {
|
||||
return {
|
||||
position: "fixed",
|
||||
left: viewportPadding,
|
||||
top: viewportPadding,
|
||||
width,
|
||||
zIndex,
|
||||
};
|
||||
}
|
||||
|
||||
function handleEscape(event: KeyboardEvent) {
|
||||
if (event.key === "Escape") {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("mousedown", handlePointerDown);
|
||||
window.addEventListener("keydown", handleEscape);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handlePointerDown);
|
||||
window.removeEventListener("keydown", handleEscape);
|
||||
};
|
||||
}, [ignoreElements, onClose]);
|
||||
|
||||
const style = useMemo<CSSProperties>(() => {
|
||||
let left = 0;
|
||||
let top = 0;
|
||||
|
||||
@@ -94,17 +82,134 @@ export function useViewportPopover({
|
||||
}
|
||||
}
|
||||
|
||||
const maxLeft = Math.max(viewportPadding, window.innerWidth - width - viewportPadding);
|
||||
const maxTop = Math.max(viewportPadding, window.innerHeight - estimatedHeight - viewportPadding);
|
||||
const measuredWidth = ref.current?.offsetWidth ?? width;
|
||||
const measuredHeight = ref.current?.offsetHeight ?? estimatedHeight;
|
||||
const maxLeft = Math.max(viewportPadding, window.innerWidth - measuredWidth - viewportPadding);
|
||||
const maxTop = Math.max(viewportPadding, window.innerHeight - measuredHeight - viewportPadding);
|
||||
|
||||
return {
|
||||
position: "fixed",
|
||||
left: Math.min(Math.max(left, viewportPadding), maxLeft),
|
||||
top: Math.min(Math.max(top, viewportPadding), maxTop),
|
||||
width,
|
||||
zIndex: 60,
|
||||
zIndex,
|
||||
};
|
||||
}, [align, anchor, estimatedHeight, offset, side, viewportPadding, width]);
|
||||
};
|
||||
|
||||
const [style, setStyle] = useState<CSSProperties>(() => computeStyle());
|
||||
|
||||
useEffect(() => {
|
||||
function handlePointerDown(event: PointerEvent) {
|
||||
const target = event.target;
|
||||
if (!(target instanceof Node)) {
|
||||
return;
|
||||
}
|
||||
if (ref.current?.contains(target)) {
|
||||
return;
|
||||
}
|
||||
if (ignoreElements.some((element) => element?.contains(target))) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
target instanceof Element
|
||||
&& ignoreSelectors.some((selector) => target.closest(selector) !== null)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
|
||||
function handleEscape(event: KeyboardEvent) {
|
||||
if (event.key === "Escape") {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("pointerdown", handlePointerDown, true);
|
||||
window.addEventListener("keydown", handleEscape);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", handlePointerDown, true);
|
||||
window.removeEventListener("keydown", handleEscape);
|
||||
};
|
||||
}, [ignoreElements, ignoreSelectors, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
setStyle(computeStyle());
|
||||
}, [align, anchor, estimatedHeight, offset, side, viewportPadding, width, zIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (!element || typeof ResizeObserver === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
setStyle(computeStyle());
|
||||
});
|
||||
observer.observe(element);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [align, anchor, estimatedHeight, offset, side, viewportPadding, width, zIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
function cancelScheduledFrame() {
|
||||
if (frameRef.current === null) return;
|
||||
cancelAnimationFrame(frameRef.current);
|
||||
frameRef.current = null;
|
||||
}
|
||||
|
||||
function updateOrClose() {
|
||||
if (anchor.kind === "element" && !anchor.element.isConnected) {
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
setStyle(computeStyle());
|
||||
}
|
||||
|
||||
function scheduleUpdate(reason: "scroll" | "resize") {
|
||||
if (frameRef.current !== null) return;
|
||||
frameRef.current = requestAnimationFrame(() => {
|
||||
frameRef.current = null;
|
||||
updateOrClose();
|
||||
});
|
||||
}
|
||||
|
||||
updateOrClose();
|
||||
|
||||
const handleScroll = () => {
|
||||
scheduleUpdate("scroll");
|
||||
};
|
||||
const handleResize = () => {
|
||||
scheduleUpdate("resize");
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll, true);
|
||||
window.addEventListener("resize", handleResize, { passive: true });
|
||||
window.visualViewport?.addEventListener("resize", handleResize, { passive: true });
|
||||
window.visualViewport?.addEventListener("scroll", handleScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
cancelScheduledFrame();
|
||||
window.removeEventListener("scroll", handleScroll, true);
|
||||
window.removeEventListener("resize", handleResize);
|
||||
window.visualViewport?.removeEventListener("resize", handleResize);
|
||||
window.visualViewport?.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, [
|
||||
align,
|
||||
anchor,
|
||||
estimatedHeight,
|
||||
offset,
|
||||
onClose,
|
||||
side,
|
||||
viewportPadding,
|
||||
width,
|
||||
zIndex,
|
||||
]);
|
||||
|
||||
return { ref, style };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user