Fix: Polish projects menu
This commit is contained in:
+120
@@ -0,0 +1,120 @@
|
||||
import { createEffect, createSignal, onCleanup } from "solid-js";
|
||||
import type { ProjectMenuTarget } from "../data/shell.data";
|
||||
|
||||
type ProjectContextMenuState = {
|
||||
target: ProjectMenuTarget;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
const readRootPixelToken = (name: string, fallback: number): number => {
|
||||
if (typeof window === "undefined") {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const value = window.getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
const parsed = Number.parseFloat(value);
|
||||
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (value.endsWith("px")) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return parsed * 16;
|
||||
};
|
||||
|
||||
const clampMenuPosition = (value: number, min: number, max: number): number => {
|
||||
if (max <= min) {
|
||||
return min;
|
||||
}
|
||||
|
||||
return Math.min(Math.max(value, min), max);
|
||||
};
|
||||
|
||||
export const createProjectContextMenuController = () => {
|
||||
const [menuState, setMenuState] = createSignal<ProjectContextMenuState | null>(null);
|
||||
let menuRef: HTMLDivElement | undefined;
|
||||
|
||||
const closeMenu = (): void => {
|
||||
setMenuState(null);
|
||||
};
|
||||
|
||||
const repositionMenu = (): void => {
|
||||
if (typeof window === "undefined" || !menuRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
const current = menuState();
|
||||
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewportPadding = readRootPixelToken("--space-4", 16);
|
||||
const rect = menuRef.getBoundingClientRect();
|
||||
const nextX = clampMenuPosition(current.x, viewportPadding, window.innerWidth - rect.width - viewportPadding);
|
||||
const nextY = clampMenuPosition(current.y, viewportPadding, window.innerHeight - rect.height - viewportPadding);
|
||||
|
||||
if (nextX === current.x && nextY === current.y) {
|
||||
return;
|
||||
}
|
||||
|
||||
setMenuState({ ...current, x: nextX, y: nextY });
|
||||
};
|
||||
|
||||
const openMenu = (event: MouseEvent, target: ProjectMenuTarget): void => {
|
||||
event.preventDefault();
|
||||
setMenuState({ target, x: event.clientX, y: event.clientY });
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
if (!menuState() || typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
const frame = window.requestAnimationFrame(() => {
|
||||
repositionMenu();
|
||||
});
|
||||
|
||||
const handlePointerDown = (event: PointerEvent): void => {
|
||||
if (!menuRef?.contains(event.target as Node)) {
|
||||
closeMenu();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent): void => {
|
||||
if (event.key === "Escape") {
|
||||
closeMenu();
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewportChange = (): void => {
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
document.addEventListener("pointerdown", handlePointerDown);
|
||||
window.addEventListener("resize", handleViewportChange);
|
||||
window.addEventListener("scroll", handleViewportChange, true);
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
onCleanup(() => {
|
||||
window.cancelAnimationFrame(frame);
|
||||
document.removeEventListener("pointerdown", handlePointerDown);
|
||||
window.removeEventListener("resize", handleViewportChange);
|
||||
window.removeEventListener("scroll", handleViewportChange, true);
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
menuState,
|
||||
openMenu,
|
||||
closeMenu,
|
||||
setMenuRef: (element: HTMLDivElement): void => {
|
||||
menuRef = element;
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user