mirror of
https://github.com/outline/outline.git
synced 2025-12-30 07:19:52 -06:00
Add mouse safe area for when moving between contextual submenus
This commit is contained in:
66
app/components/ContextMenu/MouseSafeArea.tsx
Normal file
66
app/components/ContextMenu/MouseSafeArea.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import * as React from "react";
|
||||
import { useMousePosition } from "~/hooks/useMousePosition";
|
||||
|
||||
type Positions = {
|
||||
/* Sub-menu x */
|
||||
x: number;
|
||||
/* Sub-menu y */
|
||||
y: number;
|
||||
/* Sub-menu height */
|
||||
h: number;
|
||||
/* Sub-menu width */
|
||||
w: number;
|
||||
/* Mouse x */
|
||||
mouseX: number;
|
||||
/* Mouse y */
|
||||
mouseY: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Component to cover the area between the mouse cursor and the sub-menu, to
|
||||
* allow moving cursor to lower parts of sub-menu without the sub-menu
|
||||
* disappearing.
|
||||
*/
|
||||
export default function MouseSafeArea(props: {
|
||||
parentRef: React.RefObject<HTMLElement | null>;
|
||||
}) {
|
||||
const { x = 0, y = 0, height: h = 0, width: w = 0 } =
|
||||
props.parentRef.current?.getBoundingClientRect() || {};
|
||||
const [mouseX, mouseY] = useMousePosition();
|
||||
const positions = { x, y, h, w, mouseX, mouseY };
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
// backgroundColor: "rgba(255,0,0,0.1)", // Uncomment to debug
|
||||
right: getRight(positions),
|
||||
left: getLeft(positions),
|
||||
height: h,
|
||||
width: getWidth(positions),
|
||||
clipPath: getClipPath(positions),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const getLeft = ({ x, mouseX }: Positions) =>
|
||||
mouseX > x ? undefined : -Math.max(x - mouseX, 10) + "px";
|
||||
|
||||
const getRight = ({ x, w, mouseX }: Positions) =>
|
||||
mouseX > x ? -Math.max(mouseX - (x + w), 10) + "px" : undefined;
|
||||
|
||||
const getWidth = ({ x, w, mouseX }: Positions) =>
|
||||
mouseX > x
|
||||
? Math.max(mouseX - (x + w), 10) + "px"
|
||||
: Math.max(x - mouseX, 10) + "px";
|
||||
|
||||
const getClipPath = ({ x, y, h, mouseX, mouseY }: Positions) =>
|
||||
mouseX > x
|
||||
? `polygon(0% 0%, 0% 100%, 100% ${(100 * (mouseY - y)) / h - 10}%, 100% ${
|
||||
(100 * (mouseY - y)) / h + 5
|
||||
}%)`
|
||||
: `polygon(100% 0%, 0% ${(100 * (mouseY - y)) / h - 10}%, 0% ${
|
||||
(100 * (mouseY - y)) / h + 5
|
||||
}%, 100% 100%)`;
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from "~/types";
|
||||
import Header from "./Header";
|
||||
import MenuItem, { MenuAnchor } from "./MenuItem";
|
||||
import MouseSafeArea from "./MouseSafeArea";
|
||||
import Separator from "./Separator";
|
||||
import ContextMenu from ".";
|
||||
|
||||
@@ -59,6 +60,7 @@ const Submenu = React.forwardRef(
|
||||
)}
|
||||
</MenuButton>
|
||||
<ContextMenu {...menu} aria-label={t("Submenu")}>
|
||||
<MouseSafeArea parentRef={menu.unstable_popoverRef} />
|
||||
<Template {...menu} items={templateItems} />
|
||||
</ContextMenu>
|
||||
</>
|
||||
|
||||
31
app/hooks/useMousePosition.ts
Normal file
31
app/hooks/useMousePosition.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { throttle } from "lodash";
|
||||
import * as React from "react";
|
||||
|
||||
/**
|
||||
* Mouse position as a tuple of [x, y]
|
||||
*/
|
||||
type MousePosition = [number, number];
|
||||
|
||||
/**
|
||||
* Hook to get the current mouse position
|
||||
*
|
||||
* @returns Mouse position as a tuple of [x, y]
|
||||
*/
|
||||
export const useMousePosition = () => {
|
||||
const [mousePosition, setMousePosition] = React.useState<MousePosition>([
|
||||
0,
|
||||
0,
|
||||
]);
|
||||
|
||||
const updateMousePosition = throttle((ev: MouseEvent) => {
|
||||
setMousePosition([ev.clientX, ev.clientY]);
|
||||
}, 200);
|
||||
|
||||
React.useEffect(() => {
|
||||
window.addEventListener("mousemove", updateMousePosition);
|
||||
|
||||
return () => window.removeEventListener("mousemove", updateMousePosition);
|
||||
}, []);
|
||||
|
||||
return mousePosition;
|
||||
};
|
||||
Reference in New Issue
Block a user