Skip to content

Commit

Permalink
style: Let the context menu dispaly its items within the viewport
Browse files Browse the repository at this point in the history
  • Loading branch information
miyanokomiya committed Nov 19, 2024
1 parent aa4c4b5 commit 35df328
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 19 deletions.
54 changes: 43 additions & 11 deletions src/components/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const ContextMenu: React.FC<Props> = ({ items, point, onClickItem, viewSi
);

const ref = useRef<HTMLDivElement>(null);
const { diff } = usePanelWithinViewport(ref, viewSize);
const [diff] = usePanelWithinViewport(ref, viewSize);
const p = diff ? add(diff, point) : point;

return (
Expand All @@ -35,18 +35,19 @@ export const ContextMenu: React.FC<Props> = ({ items, point, onClickItem, viewSi
}}
>
<div className="flex flex-col">
<ContextList items={items} onClickItem={handleClick} />
<ContextList items={items} viewSize={viewSize} onClickItem={handleClick} />
</div>
</div>
);
};

interface ContextListProps {
items: ContextMenuItem[];
viewSize: Size;
onClickItem?: (item: ContextMenuItem) => void;
}

const ContextList: React.FC<ContextListProps> = ({ items, onClickItem }) => {
const ContextList: React.FC<ContextListProps> = ({ items, viewSize, onClickItem }) => {
const [dropdownKey, setDropdownKey] = useState("");

const handleClick = useCallback(
Expand All @@ -62,18 +63,23 @@ const ContextList: React.FC<ContextListProps> = ({ items, onClickItem }) => {
[onClickItem],
);

return items.map((item, i) => (
<ContextItem key={i} item={item} dropdownKey={dropdownKey} onClickItem={handleClick} />
));
return (
<div className="min-w-24">
{items.map((item, i) => (
<ContextItem key={i} item={item} viewSize={viewSize} dropdownKey={dropdownKey} onClickItem={handleClick} />
))}
</div>
);
};

interface ContextItemProps {
item: ContextMenuItem;
dropdownKey?: string;
viewSize: Size;
onClickItem?: (item: ContextMenuItem) => void;
}

const ContextItem: React.FC<ContextItemProps> = ({ item, dropdownKey, onClickItem }) => {
const ContextItem: React.FC<ContextItemProps> = ({ item, viewSize, dropdownKey, onClickItem }) => {
const handleClick = useCallback(() => {
onClickItem?.(item);
}, [item, onClickItem]);
Expand All @@ -88,6 +94,7 @@ const ContextItem: React.FC<ContextItemProps> = ({ item, dropdownKey, onClickIte

return (
<div className="relative">
{/* This div prevents redundant white space when child list displays. */}
<div>
<ListButton onClick={handleClick}>
<div className="flex items-center justify-between gap-2 w-full">
Expand All @@ -101,14 +108,39 @@ const ContextItem: React.FC<ContextItemProps> = ({ item, dropdownKey, onClickIte
</ListButton>
</div>
{dropdownKey === item.key ? (
<div className="absolute left-full top-1/2 -translate-y-1/2 border bg-white w-max">
<ContextList items={item.children} onClickItem={onClickItem} />
</div>
<ChildContextList items={item.children} viewSize={viewSize} onClickItem={onClickItem} />
) : undefined}
</div>
);
};

interface ChildContextList {
items: ContextMenuItem[];
viewSize: Size;
onClickItem?: (item: ContextMenuItem) => void;
}

const ChildContextList: React.FC<ChildContextList> = ({ items, viewSize, onClickItem }) => {
const ref = useRef<HTMLDivElement>(null);
const [style, setStyle] = useState("opacity-0 left-full top-1/2 -translate-y-1/2");

useEffect(() => {
if (!ref.current) return;

const rect = ref.current.getBoundingClientRect();
setStyle(
(rect.right > viewSize.width + PANEL_OFFSET ? "right-full " : "left-full ") +
(rect.bottom > viewSize.height + PANEL_OFFSET ? "bottom-0" : "top-1/2 -translate-y-1/2"),
);
}, [viewSize]);

return (
<div ref={ref} className={"absolute border bg-white w-max " + style}>
<ContextList items={items} viewSize={viewSize} onClickItem={onClickItem} />
</div>
);
};

const PANEL_OFFSET = 4;

const usePanelWithinViewport = (panelRef: React.RefObject<HTMLElement>, viewSize: Size) => {
Expand All @@ -129,5 +161,5 @@ const usePanelWithinViewport = (panelRef: React.RefObject<HTMLElement>, viewSize
setDiff({ x: dx, y: dy });
}, [panelRef, viewSize]);

return { diff };
return [diff];
};
4 changes: 2 additions & 2 deletions src/composables/lineSnapping.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,13 @@ describe("newLineSnapping", () => {
});

// Self snapped & Outline snapped
expect(target.testConnection({ x: 160, y: -5 }, 1)).toEqual({
expect(target.testConnection({ x: 159, y: -5 }, 1)).toEqual({
connection: { id: "b", rate: { x: 0, y: 0 } },
p: { x: 150, y: 0 },
guidLines: [
[
{ x: 0, y: 0 },
{ x: 160, y: 0 },
{ x: 159, y: 0 },
],
],
});
Expand Down
16 changes: 10 additions & 6 deletions src/composables/lineSnapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,10 @@ export function newLineSnapping(option: Option) {
extendedGuideLine[1],
);
if (candidates) {
intersection = candidates.find((c) => getDistance(c, lineConstrain!.p) <= threshold);
if (intersection) {
const origin = lineConstrain.p;
const closestCandidate = pickMinItem(candidates, (c) => getD2(sub(c, origin)));
if (closestCandidate && getDistance(closestCandidate, origin) < threshold) {
intersection = closestCandidate;
priorityGuidline = lineConstrain.guidLines[0];
}
}
Expand Down Expand Up @@ -238,10 +240,12 @@ export function newLineSnapping(option: Option) {
}

if (outline) {
const connection: ConnectionPoint = {
rate: shapeComposite.getLocationRateOnShape(outline.shape, outline.p),
id: outline.shape.id,
};
const connection: ConnectionPoint | undefined = isLineShape(outline.shape)
? undefined
: {
rate: shapeComposite.getLocationRateOnShape(outline.shape, outline.p),
id: outline.shape.id,
};

if (lineConstrain) {
return {
Expand Down

0 comments on commit 35df328

Please sign in to comment.