Skip to content

Commit 82117b9

Browse files
committed
fix context menu and popups (use react portal)
1 parent 97f91b5 commit 82117b9

7 files changed

Lines changed: 88 additions & 20 deletions

File tree

src/common/utils/stringUtils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export function toKebabCase(str: string) {
2+
return str
3+
.normalize("NFKD") // split accented chars
4+
.replace(/[\u0300-\u036f]/g, "") // remove accents
5+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2") // camelCase → camel-Case
6+
.replace(/[^a-zA-Z0-9]+/g, "-") // non-alphanumerics → -
7+
.replace(/^-+|-+$/g, "") // trim leading/trailing -
8+
.toLowerCase();
9+
}

src/components/AppPopup/AppPopup.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import React, { useEffect, useRef, useState } from "react";
2+
import { createPortal } from "react-dom";
23
import { Rect } from "../../hooks/useDragToResize";
34
import useDetectMouseDownOutside from "../../hooks/useDetectMouseDownOutside";
5+
46
import useSystemSettings from "../../stores/systemSettingsStore";
7+
import useWindowManagerStore from "../../stores/windowManagerStore";
58

69
import { StyledContent, StyledPopup } from "./styles";
710

@@ -15,6 +18,7 @@ function AppPopup<Element extends HTMLElement>({
1518
children,
1619
close,
1720
}: React.PropsWithChildren<AppPopupProps<Element>>) {
21+
const desktopRef = useWindowManagerStore((s) => s.desktopRef);
1822
const thisRef = useRef<HTMLDivElement>(null);
1923
const settings = useSystemSettings();
2024
const [rect, setRect] = useState<Rect>({
@@ -43,7 +47,10 @@ function AppPopup<Element extends HTMLElement>({
4347
e.preventDefault();
4448
}
4549

46-
return (
50+
// popup is portalled to desktop element
51+
if (!desktopRef.current) return;
52+
53+
return createPortal(
4754
<StyledPopup
4855
{...rect}
4956
ref={thisRef}
@@ -54,7 +61,8 @@ function AppPopup<Element extends HTMLElement>({
5461
<StyledContent backgroundColor={settings.mainColor}>
5562
{children}
5663
</StyledContent>
57-
</StyledPopup>
64+
</StyledPopup>,
65+
desktopRef.current,
5866
);
5967
}
6068

src/components/AppPopup/styles.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import styled from "@emotion/styled";
22
import { Rect } from "../../hooks/useDragToResize";
33

44
export const StyledPopup = styled.div<Rect>`
5-
backdrop-filter: brightness(80%);
65
position: fixed;
76
z-index: 9999;
87
display: flex;
@@ -12,6 +11,9 @@ export const StyledPopup = styled.div<Rect>`
1211
left: ${(props) => props.left}px;
1312
width: ${(props) => props.width}px;
1413
height: ${(props) => props.height}px;
14+
backdrop-filter: blur(5px) brightness(80%);
15+
border-radius: 6px;
16+
box-shadow: 0 0 3px 3px rgb(0, 0, 0, 0.3);
1517
`;
1618

1719
export const StyledContent = styled.div<{ backgroundColor: string }>`

src/components/BorderedApp/BorderedApp.tsx

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
StyledWindowMenusWrapper,
2323
StyledContentInner,
2424
} from "./styles";
25+
import { toKebabCase } from "../../common/utils/stringUtils";
2526

2627
/**
2728
* A type for each of the programs wrapped in a bordered app to forward a ref for,
@@ -128,13 +129,30 @@ function BorderedApp<
128129
onKeyDown={handleKeyDown}
129130
opacity={opacity}
130131
blur={blur}
132+
className={`bordered-app bordered-app_${toKebabCase(title)}`}
131133
>
132-
<StyledCorner location="nw" ref={resizeHandleNW} />
133-
<StyledEdge location="n" ref={resizeHandleN} />
134-
<StyledCorner location="ne" ref={resizeHandleNE} />
135-
<StyledEdge location="e" ref={resizeHandleE} />
134+
<StyledCorner
135+
location="nw"
136+
ref={resizeHandleNW}
137+
className="bordered-app__corner--nw"
138+
/>
139+
<StyledEdge
140+
location="n"
141+
ref={resizeHandleN}
142+
className="bordered-app__edge--n"
143+
/>
144+
<StyledCorner
145+
location="ne"
146+
ref={resizeHandleNE}
147+
className="bordered-app__corner--ne"
148+
/>
149+
<StyledEdge
150+
location="e"
151+
ref={resizeHandleE}
152+
className="bordered-app__edge--e"
153+
/>
136154
<StyledTitleBar
137-
className="drag-to-move"
155+
className="bordered-app__title-bar drag-to-move"
138156
ref={moveHandle}
139157
onDoubleClick={maximize}
140158
>
@@ -175,10 +193,26 @@ function BorderedApp<
175193
{children}
176194
</StyledContentInner>
177195
</StyledContent>
178-
<StyledCorner location="sw" ref={resizeHandleSW} />
179-
<StyledEdge location="s" ref={resizeHandleS} />
180-
<StyledCorner location="se" ref={resizeHandleSE} />
181-
<StyledEdge location="w" ref={resizeHandleW} />
196+
<StyledCorner
197+
location="sw"
198+
ref={resizeHandleSW}
199+
className="bordered-app__corner--sw"
200+
/>
201+
<StyledEdge
202+
location="s"
203+
ref={resizeHandleS}
204+
className="bordered-app__edge--s"
205+
/>
206+
<StyledCorner
207+
location="se"
208+
ref={resizeHandleSE}
209+
className="bordered-app__corner--se"
210+
/>
211+
<StyledEdge
212+
location="w"
213+
ref={resizeHandleW}
214+
className="bordered-app__edge--w"
215+
/>
182216
</StyledBorderedApp>
183217
);
184218
}

src/components/ContextMenu/ContextMenu.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import useDetectMouseDownOutside from "../../hooks/useDetectMouseDownOutside";
44

55
import { StyledContextMenu } from "./styles";
66
import useBindKeyToAction from "../../hooks/useBindKeyToAction";
7+
import { createPortal } from "react-dom";
8+
import useWindowManagerStore from "../../stores/windowManagerStore";
79

810
interface ContextMenuProps {
911
items: Array<MenuItemProps>;
@@ -17,14 +19,23 @@ function ContextMenu({ items, position, close }: ContextMenuProps) {
1719
useDetectMouseDownOutside({ elementRef, onMouseDown: close });
1820
useBindKeyToAction({ keys: ["Escape"], action: close });
1921

20-
return (
21-
<StyledContextMenu position={position} ref={elementRef}>
22+
// Context menu portalled to the desktop element.
23+
const desktopRef = useWindowManagerStore((s) => s.desktopRef);
24+
if (!desktopRef.current) return;
25+
26+
return createPortal(
27+
<StyledContextMenu
28+
position={position}
29+
ref={elementRef}
30+
className="context-menu"
31+
>
2232
<MenuItems
2333
items={items}
2434
position={{ x: 0, y: 0 }}
2535
positionType="relative"
2636
/>
27-
</StyledContextMenu>
37+
</StyledContextMenu>,
38+
desktopRef.current,
2839
);
2940
}
3041

src/components/Desktop/Desktop.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ import BottomBar from "../BottomBar";
44
import useSystemSettings from "../../stores/systemSettingsStore";
55

66
import { StyledBackground, StyledDesktop } from "./styles";
7+
import useWindowManagerStore from "../../stores/windowManagerStore";
78

89
interface DesktopProps {}
910

1011
// eslint-disable-next-line no-empty-pattern
1112
function Desktop({}: DesktopProps) {
1213
const settings = useSystemSettings();
14+
const desktopRef = useWindowManagerStore((s) => s.desktopRef);
1315
return (
14-
<StyledDesktop id="desktop">
16+
<StyledDesktop id="desktop" ref={desktopRef}>
1517
<StyledBackground id="background" backgroundUrl={settings.background} />
1618
<TopBar />
1719
<Content />

src/stores/windowManagerStore.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@ interface ComponentDefinition<Props extends BaseProps = BaseProps> {
1616

1717
interface WindowManagerStoreState {
1818
contentRef: React.RefObject<HTMLDivElement>;
19+
desktopRef: React.RefObject<HTMLDivElement>;
1920
windowsMap: Map<string, Map<string, ComponentDefinition>>;
2021
highestZIndex: number;
2122
getWindowDefinitions: () => Array<ComponentDefinition>;
2223
addWindow: <Props extends BaseProps = BaseProps>(
2324
windowType: string,
2425
windowId: string,
25-
definition: ComponentDefinition<Props>
26+
definition: ComponentDefinition<Props>,
2627
) => void;
2728
closeWindow: (windowType: string, windowId: string) => void;
2829
focusWindowsOfType: (windowType: string) => void;
@@ -33,17 +34,18 @@ interface WindowManagerStoreState {
3334

3435
const useWindowManagerStore = create<WindowManagerStoreState>()((set, get) => ({
3536
contentRef: React.createRef<HTMLDivElement>(),
37+
desktopRef: React.createRef<HTMLDivElement>(),
3638
windowsMap: new Map(),
3739
highestZIndex: 0,
3840
getWindowDefinitions() {
3941
return Array.from(get().windowsMap.values()).flatMap((map) =>
40-
Array.from(map.values())
42+
Array.from(map.values()),
4143
);
4244
},
4345
addWindow<Props extends BaseProps = BaseProps>(
4446
windowType: string,
4547
windowId: string,
46-
definition: ComponentDefinition<Props>
48+
definition: ComponentDefinition<Props>,
4749
) {
4850
const windowsMap = get().windowsMap;
4951
const windowsOfType = windowsMap.get(windowType);
@@ -54,7 +56,7 @@ const useWindowManagerStore = create<WindowManagerStoreState>()((set, get) => ({
5456
if (!windowsOfType) {
5557
windowsMap.set(
5658
windowType,
57-
new Map([[windowId, definition as unknown as ComponentDefinition]])
59+
new Map([[windowId, definition as unknown as ComponentDefinition]]),
5860
);
5961
} else {
6062
windowsOfType.set(windowId, definition as unknown as ComponentDefinition);

0 commit comments

Comments
 (0)