Skip to content

Commit 3ec441b

Browse files
committed
Moved menu items to their own component
1 parent e75bba8 commit 3ec441b

4 files changed

Lines changed: 167 additions & 145 deletions

File tree

src/components/BorderedApp/BorderedAppMenu/BorderedAppMenu.tsx

Lines changed: 3 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -1,148 +1,11 @@
11
import { useEffect, useRef, useState } from "react";
22
import "./BorderedAppMenu.scss";
3-
import useSystemSettings from "../../../stores/systemSettingsStore";
43
import useDetectClickOutside from "../../../hooks/useDetectClickOutside";
5-
6-
export interface BorderedAppMenuItemProps {
7-
title: string;
8-
action?: () => void;
9-
items?: Array<BorderedAppMenuItemProps>;
10-
}
4+
import MenuItems, { MenuItemProps } from "../../MenuItems";
115

126
export interface BorderedAppMenuProps {
137
title: string;
14-
items: Array<BorderedAppMenuItemProps>;
15-
}
16-
17-
function BorderedAppMenuItem({
18-
title,
19-
action,
20-
items,
21-
itemNo,
22-
position,
23-
}: BorderedAppMenuItemProps & {
24-
itemNo: number;
25-
position: { x: number; y: number };
26-
}) {
27-
const hoverDelayMs = 400;
28-
const elementRef = useRef<HTMLDivElement>(null);
29-
const elementPosition = useRef({ ...position });
30-
const hoverOpenDelayRef = useRef<NodeJS.Timeout>();
31-
const hoverCloseDelayRef = useRef<NodeJS.Timeout>();
32-
const [open, setOpen] = useState<boolean>(false);
33-
34-
useEffect(() => {
35-
return () => {
36-
clearTimeout(hoverCloseDelayRef.current);
37-
clearTimeout(hoverOpenDelayRef.current);
38-
};
39-
}, []);
40-
41-
useEffect(() => {
42-
const rect = elementRef.current?.getBoundingClientRect();
43-
elementPosition.current.x = rect?.width ?? 0;
44-
elementPosition.current.y = (rect?.height ?? 0) * (itemNo - 1);
45-
}, [elementRef, itemNo, position]);
46-
47-
function handleOnMouseEnter() {
48-
// Now that we've entered the item, if it's open
49-
// and there's a pending timeout to close it,
50-
// we want to cancel that
51-
if (open && hoverCloseDelayRef.current) {
52-
clearTimeout(hoverCloseDelayRef.current);
53-
hoverCloseDelayRef.current = undefined;
54-
}
55-
56-
// If it's not open yet and there's no pending
57-
// timeout to open it, lets start one.
58-
if (!open && !hoverOpenDelayRef.current) {
59-
hoverOpenDelayRef.current = setTimeout(() => {
60-
if (!open) {
61-
setOpen(true);
62-
hoverOpenDelayRef.current = undefined;
63-
}
64-
}, hoverDelayMs);
65-
}
66-
}
67-
function handleOnMouseLeave() {
68-
// Now that we've left the item, if it's closed
69-
// and there's a pending timeout to open it,
70-
// we want to cancel that
71-
if (!open && hoverOpenDelayRef.current) {
72-
clearTimeout(hoverOpenDelayRef.current);
73-
hoverOpenDelayRef.current = undefined;
74-
}
75-
76-
// If it's not closed yet and there's no pending
77-
// timeout to close it, lets start one.
78-
if (open && !hoverCloseDelayRef.current) {
79-
hoverCloseDelayRef.current = setTimeout(() => {
80-
if (open) {
81-
setOpen(false);
82-
hoverCloseDelayRef.current = undefined;
83-
}
84-
}, hoverDelayMs);
85-
}
86-
}
87-
88-
function handleOnClick(e: React.MouseEvent<HTMLDivElement>) {
89-
e.stopPropagation();
90-
if (!items) return;
91-
setOpen(!open);
92-
}
93-
94-
return (
95-
<div
96-
ref={elementRef}
97-
className="bordered-app-menu-item"
98-
onClick={handleOnClick}
99-
onMouseEnter={handleOnMouseEnter}
100-
onMouseLeave={handleOnMouseLeave}
101-
>
102-
<span className="bordered-app-menu-item__title" onClick={action}>
103-
{title}
104-
</span>
105-
{items && open && (
106-
<BorderedAppMenuItems
107-
items={items}
108-
position={{ ...elementPosition.current }}
109-
/>
110-
)}
111-
</div>
112-
);
113-
}
114-
115-
function BorderedAppMenuItems({
116-
items,
117-
position,
118-
}: {
119-
items: Array<BorderedAppMenuItemProps>;
120-
position: { x: number; y: number };
121-
}) {
122-
const settings = useSystemSettings();
123-
return (
124-
<div
125-
className="bordered-app-menu-items"
126-
style={{
127-
left: position.x,
128-
top: position.y,
129-
backgroundColor: settings.mainColor,
130-
}}
131-
>
132-
<div className="bordered-app-menu-items__content">
133-
{items.map((item, i) => (
134-
<BorderedAppMenuItem
135-
title={item.title}
136-
action={item.action}
137-
items={item.items}
138-
itemNo={i + 1}
139-
position={{ ...position }}
140-
key={`${item.title}-${i}`}
141-
/>
142-
))}
143-
</div>
144-
</div>
145-
);
8+
items: Array<MenuItemProps>;
1469
}
14710

14811
function BorderedAppMenu({ title, items }: BorderedAppMenuProps) {
@@ -166,12 +29,7 @@ function BorderedAppMenu({ title, items }: BorderedAppMenuProps) {
16629
onClick={() => setOpen(!open)}
16730
>
16831
<span className="bordered-app-menu__title">{title}</span>
169-
{open && (
170-
<BorderedAppMenuItems
171-
items={items}
172-
position={{ ...position.current }}
173-
/>
174-
)}
32+
{open && <MenuItems items={items} position={{ ...position.current }} />}
17533
</div>
17634
);
17735
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
.menu {
2+
&-items {
3+
width: auto;
4+
position: absolute;
5+
white-space: nowrap;
6+
box-sizing: border-box;
7+
box-shadow: 0 0 2px rgb(0, 0, 0, 0.5);
8+
9+
&__content {
10+
box-sizing: border-box;
11+
}
12+
}
13+
14+
&-item {
15+
padding: 5px 12px;
16+
height: 20px;
17+
&__title {
18+
box-sizing: border-box;
19+
}
20+
}
21+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { useEffect, useRef, useState } from "react";
2+
import useSystemSettings from "../../stores/systemSettingsStore";
3+
import "./MenuItems.scss";
4+
5+
export interface MenuItemsProps {
6+
items: Array<MenuItemProps>;
7+
position: { x: number; y: number };
8+
}
9+
10+
export interface MenuItemProps {
11+
title: string;
12+
action?: () => void;
13+
items?: Array<MenuItemProps>;
14+
}
15+
16+
function MenuItems({ items, position }: MenuItemsProps) {
17+
const settings = useSystemSettings();
18+
return (
19+
<div
20+
className="menu-items"
21+
style={{
22+
left: position.x,
23+
top: position.y,
24+
backgroundColor: settings.mainColor,
25+
}}
26+
>
27+
<div className="menu-items__content">
28+
{items.map((item, i) => (
29+
<MenuItem
30+
title={item.title}
31+
action={item.action}
32+
items={item.items}
33+
itemNo={i + 1}
34+
position={{ ...position }}
35+
key={`${item.title}-${i}`}
36+
/>
37+
))}
38+
</div>
39+
</div>
40+
);
41+
}
42+
43+
function MenuItem({
44+
title,
45+
action,
46+
items,
47+
itemNo,
48+
position,
49+
}: MenuItemProps & {
50+
itemNo: number;
51+
position: { x: number; y: number };
52+
}) {
53+
const hoverDelayMs = 400;
54+
const elementRef = useRef<HTMLDivElement>(null);
55+
const elementPosition = useRef({ ...position });
56+
const hoverOpenDelayRef = useRef<NodeJS.Timeout>();
57+
const hoverCloseDelayRef = useRef<NodeJS.Timeout>();
58+
const [open, setOpen] = useState<boolean>(false);
59+
60+
useEffect(() => {
61+
return () => {
62+
clearTimeout(hoverCloseDelayRef.current);
63+
clearTimeout(hoverOpenDelayRef.current);
64+
};
65+
}, []);
66+
67+
useEffect(() => {
68+
const rect = elementRef.current?.getBoundingClientRect();
69+
elementPosition.current.x = rect?.width ?? 0;
70+
elementPosition.current.y = (rect?.height ?? 0) * (itemNo - 1);
71+
}, [elementRef, itemNo, position]);
72+
73+
function handleOnMouseEnter() {
74+
// Now that we've entered the item, if it's open
75+
// and there's a pending timeout to close it,
76+
// we want to cancel that
77+
if (open && hoverCloseDelayRef.current) {
78+
clearTimeout(hoverCloseDelayRef.current);
79+
hoverCloseDelayRef.current = undefined;
80+
}
81+
82+
// If it's not open yet and there's no pending
83+
// timeout to open it, lets start one.
84+
if (!open && !hoverOpenDelayRef.current) {
85+
hoverOpenDelayRef.current = setTimeout(() => {
86+
if (!open) {
87+
setOpen(true);
88+
hoverOpenDelayRef.current = undefined;
89+
}
90+
}, hoverDelayMs);
91+
}
92+
}
93+
function handleOnMouseLeave() {
94+
// Now that we've left the item, if it's closed
95+
// and there's a pending timeout to open it,
96+
// we want to cancel that
97+
if (!open && hoverOpenDelayRef.current) {
98+
clearTimeout(hoverOpenDelayRef.current);
99+
hoverOpenDelayRef.current = undefined;
100+
}
101+
102+
// If it's not closed yet and there's no pending
103+
// timeout to close it, lets start one.
104+
if (open && !hoverCloseDelayRef.current) {
105+
hoverCloseDelayRef.current = setTimeout(() => {
106+
if (open) {
107+
setOpen(false);
108+
hoverCloseDelayRef.current = undefined;
109+
}
110+
}, hoverDelayMs);
111+
}
112+
}
113+
114+
function handleOnClick(e: React.MouseEvent<HTMLDivElement>) {
115+
e.stopPropagation();
116+
if (!items) return;
117+
setOpen(!open);
118+
}
119+
120+
return (
121+
<div
122+
ref={elementRef}
123+
className="menu-item"
124+
onClick={handleOnClick}
125+
onMouseEnter={handleOnMouseEnter}
126+
onMouseLeave={handleOnMouseLeave}
127+
>
128+
<span className="menu-item__title" onClick={action}>
129+
{title}
130+
</span>
131+
{items && open && (
132+
<MenuItems items={items} position={{ ...elementPosition.current }} />
133+
)}
134+
</div>
135+
);
136+
}
137+
138+
export default MenuItems;

src/components/MenuItems/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import MenuItems from "./MenuItems";
2+
3+
export type { MenuItemProps, MenuItemsProps } from "./MenuItems";
4+
5+
export default MenuItems;

0 commit comments

Comments
 (0)