mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 12:37:20 +00:00
minimal popper implementation for context menu placement
This commit is contained in:
@@ -12,3 +12,4 @@ export * from "./uuid";
|
||||
export { FromSchema } from "./typebox/from-schema";
|
||||
export * from "./test";
|
||||
export * from "./runtime";
|
||||
export * from "./numbers";
|
||||
|
||||
5
app/src/core/utils/numbers.ts
Normal file
5
app/src/core/utils/numbers.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function clampNumber(value: number, min: number, max: number): number {
|
||||
const lower = Math.min(min, max);
|
||||
const upper = Math.max(min, max);
|
||||
return Math.max(lower, Math.min(value, upper));
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useClickOutside } from "@mantine/hooks";
|
||||
import { clampNumber } from "core/utils";
|
||||
import {
|
||||
type ComponentPropsWithoutRef,
|
||||
Fragment,
|
||||
@@ -7,7 +8,7 @@ import {
|
||||
useState
|
||||
} from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useEvent } from "../../hooks/use-event";
|
||||
import { useEvent } from "ui/hooks/use-event";
|
||||
|
||||
export type DropdownItem =
|
||||
| (() => JSX.Element)
|
||||
@@ -43,7 +44,7 @@ export function Dropdown({
|
||||
children,
|
||||
defaultOpen = false,
|
||||
openEvent = "onClick",
|
||||
position = "bottom-start",
|
||||
position: initialPosition = "bottom-start",
|
||||
dropdownWrapperProps,
|
||||
items,
|
||||
title,
|
||||
@@ -54,24 +55,58 @@ export function Dropdown({
|
||||
className
|
||||
}: DropdownProps) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
const [position, setPosition] = useState(initialPosition);
|
||||
const clickoutsideRef = useClickOutside(() => setOpen(false));
|
||||
const menuItems = items.filter(Boolean) as DropdownItem[];
|
||||
const [_offset, _setOffset] = useState(0);
|
||||
|
||||
const toggle = useEvent((delay: number = 50) =>
|
||||
setTimeout(() => setOpen((prev) => !prev), typeof delay === "number" ? delay : 0)
|
||||
);
|
||||
|
||||
const openEventHandler = useEvent((e) => {
|
||||
const onClickHandler = openEvent === "onClick" ? toggle : undefined;
|
||||
const onContextMenuHandler = useEvent((e) => {
|
||||
if (openEvent !== "onContextMenu") return;
|
||||
e.preventDefault();
|
||||
|
||||
if (open) {
|
||||
toggle(0);
|
||||
setTimeout(() => {
|
||||
setPosition(initialPosition);
|
||||
_setOffset(0);
|
||||
}, 10);
|
||||
return;
|
||||
}
|
||||
|
||||
// minimal popper impl, get pos and boundaries
|
||||
const x = e.clientX - e.currentTarget.getBoundingClientRect().left;
|
||||
const { left = 0, right = 0 } = clickoutsideRef.current?.getBoundingClientRect() ?? {};
|
||||
|
||||
// only if boundaries gien
|
||||
if (left > 0 && right > 0) {
|
||||
const safe = clampNumber(x, left, right);
|
||||
// if pos less than half, go left
|
||||
if (x < (left + right) / 2) {
|
||||
setPosition("bottom-start");
|
||||
_setOffset(safe);
|
||||
} else {
|
||||
setPosition("bottom-end");
|
||||
_setOffset(right - safe);
|
||||
}
|
||||
} else {
|
||||
setPosition(initialPosition);
|
||||
_setOffset(0);
|
||||
}
|
||||
|
||||
toggle();
|
||||
});
|
||||
|
||||
const offset = 4;
|
||||
const dropdownStyle = {
|
||||
"bottom-start": { top: "100%", left: 0, marginTop: offset },
|
||||
"bottom-end": { right: 0, top: "100%", marginTop: offset },
|
||||
"bottom-start": { top: "100%", left: _offset, marginTop: offset },
|
||||
"bottom-end": { right: _offset, top: "100%", marginTop: offset },
|
||||
"top-start": { bottom: "100%", marginBottom: offset },
|
||||
"top-end": { bottom: "100%", right: 0, marginBottom: offset }
|
||||
"top-end": { bottom: "100%", right: _offset, marginBottom: offset }
|
||||
}[position];
|
||||
|
||||
const internalOnClickItem = useEvent((item) => {
|
||||
@@ -116,12 +151,9 @@ export function Dropdown({
|
||||
role="dropdown"
|
||||
className={twMerge("relative flex", className)}
|
||||
ref={clickoutsideRef}
|
||||
onContextMenu={openEvent === "onContextMenu" ? openEventHandler : undefined}
|
||||
onContextMenu={onContextMenuHandler}
|
||||
>
|
||||
{cloneElement(
|
||||
children as any,
|
||||
openEvent === "onClick" ? { onClick: openEventHandler } : {}
|
||||
)}
|
||||
{cloneElement(children as any, { onClick: onClickHandler })}
|
||||
{open && (
|
||||
<div
|
||||
{...dropdownWrapperProps}
|
||||
@@ -131,7 +163,9 @@ export function Dropdown({
|
||||
)}
|
||||
style={dropdownStyle}
|
||||
>
|
||||
{title && <div className="text-sm font-bold px-3 mb-1 mt-1 opacity-50">{title}</div>}
|
||||
{title && (
|
||||
<div className="text-sm font-bold px-2.5 mb-1 mt-1 opacity-50">{title}</div>
|
||||
)}
|
||||
{menuItems.map((item, i) =>
|
||||
itemRenderer(item, { key: i, onClick: () => internalOnClickItem(item) })
|
||||
)}
|
||||
|
||||
@@ -205,7 +205,7 @@ const EntityContextMenu = ({
|
||||
separator,
|
||||
{
|
||||
icon: IconSettings,
|
||||
label: "Advanced settings",
|
||||
label: "Settings",
|
||||
onClick: () =>
|
||||
navigate(routes.settings.path(["data", "entities", entity.name]), {
|
||||
absolute: true
|
||||
|
||||
Reference in New Issue
Block a user