mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-17 04:46:05 +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 { FromSchema } from "./typebox/from-schema";
|
||||||
export * from "./test";
|
export * from "./test";
|
||||||
export * from "./runtime";
|
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 { useClickOutside } from "@mantine/hooks";
|
||||||
|
import { clampNumber } from "core/utils";
|
||||||
import {
|
import {
|
||||||
type ComponentPropsWithoutRef,
|
type ComponentPropsWithoutRef,
|
||||||
Fragment,
|
Fragment,
|
||||||
@@ -7,7 +8,7 @@ import {
|
|||||||
useState
|
useState
|
||||||
} from "react";
|
} from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { useEvent } from "../../hooks/use-event";
|
import { useEvent } from "ui/hooks/use-event";
|
||||||
|
|
||||||
export type DropdownItem =
|
export type DropdownItem =
|
||||||
| (() => JSX.Element)
|
| (() => JSX.Element)
|
||||||
@@ -43,7 +44,7 @@ export function Dropdown({
|
|||||||
children,
|
children,
|
||||||
defaultOpen = false,
|
defaultOpen = false,
|
||||||
openEvent = "onClick",
|
openEvent = "onClick",
|
||||||
position = "bottom-start",
|
position: initialPosition = "bottom-start",
|
||||||
dropdownWrapperProps,
|
dropdownWrapperProps,
|
||||||
items,
|
items,
|
||||||
title,
|
title,
|
||||||
@@ -54,24 +55,58 @@ export function Dropdown({
|
|||||||
className
|
className
|
||||||
}: DropdownProps) {
|
}: DropdownProps) {
|
||||||
const [open, setOpen] = useState(defaultOpen);
|
const [open, setOpen] = useState(defaultOpen);
|
||||||
|
const [position, setPosition] = useState(initialPosition);
|
||||||
const clickoutsideRef = useClickOutside(() => setOpen(false));
|
const clickoutsideRef = useClickOutside(() => setOpen(false));
|
||||||
const menuItems = items.filter(Boolean) as DropdownItem[];
|
const menuItems = items.filter(Boolean) as DropdownItem[];
|
||||||
|
const [_offset, _setOffset] = useState(0);
|
||||||
|
|
||||||
const toggle = useEvent((delay: number = 50) =>
|
const toggle = useEvent((delay: number = 50) =>
|
||||||
setTimeout(() => setOpen((prev) => !prev), typeof delay === "number" ? delay : 0)
|
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();
|
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();
|
toggle();
|
||||||
});
|
});
|
||||||
|
|
||||||
const offset = 4;
|
const offset = 4;
|
||||||
const dropdownStyle = {
|
const dropdownStyle = {
|
||||||
"bottom-start": { top: "100%", left: 0, marginTop: offset },
|
"bottom-start": { top: "100%", left: _offset, marginTop: offset },
|
||||||
"bottom-end": { right: 0, top: "100%", marginTop: offset },
|
"bottom-end": { right: _offset, top: "100%", marginTop: offset },
|
||||||
"top-start": { bottom: "100%", marginBottom: offset },
|
"top-start": { bottom: "100%", marginBottom: offset },
|
||||||
"top-end": { bottom: "100%", right: 0, marginBottom: offset }
|
"top-end": { bottom: "100%", right: _offset, marginBottom: offset }
|
||||||
}[position];
|
}[position];
|
||||||
|
|
||||||
const internalOnClickItem = useEvent((item) => {
|
const internalOnClickItem = useEvent((item) => {
|
||||||
@@ -116,12 +151,9 @@ export function Dropdown({
|
|||||||
role="dropdown"
|
role="dropdown"
|
||||||
className={twMerge("relative flex", className)}
|
className={twMerge("relative flex", className)}
|
||||||
ref={clickoutsideRef}
|
ref={clickoutsideRef}
|
||||||
onContextMenu={openEvent === "onContextMenu" ? openEventHandler : undefined}
|
onContextMenu={onContextMenuHandler}
|
||||||
>
|
>
|
||||||
{cloneElement(
|
{cloneElement(children as any, { onClick: onClickHandler })}
|
||||||
children as any,
|
|
||||||
openEvent === "onClick" ? { onClick: openEventHandler } : {}
|
|
||||||
)}
|
|
||||||
{open && (
|
{open && (
|
||||||
<div
|
<div
|
||||||
{...dropdownWrapperProps}
|
{...dropdownWrapperProps}
|
||||||
@@ -131,7 +163,9 @@ export function Dropdown({
|
|||||||
)}
|
)}
|
||||||
style={dropdownStyle}
|
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) =>
|
{menuItems.map((item, i) =>
|
||||||
itemRenderer(item, { key: i, onClick: () => internalOnClickItem(item) })
|
itemRenderer(item, { key: i, onClick: () => internalOnClickItem(item) })
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ const EntityContextMenu = ({
|
|||||||
separator,
|
separator,
|
||||||
{
|
{
|
||||||
icon: IconSettings,
|
icon: IconSettings,
|
||||||
label: "Advanced settings",
|
label: "Settings",
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
navigate(routes.settings.path(["data", "entities", entity.name]), {
|
navigate(routes.settings.path(["data", "entities", entity.name]), {
|
||||||
absolute: true
|
absolute: true
|
||||||
|
|||||||
Reference in New Issue
Block a user