Skip to content

Commit 16e489a

Browse files
committed
chore: update remaining floating menus
1 parent 5eac500 commit 16e489a

File tree

12 files changed

+7398
-9405
lines changed

12 files changed

+7398
-9405
lines changed

packages/editor/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"lowlight": "^3.0.0",
7272
"lucide-react": "^0.469.0",
7373
"prosemirror-codemark": "^0.4.2",
74+
"tippy.js": "^6.3.7",
7475
"tiptap-markdown": "^0.8.10",
7576
"uuid": "^10.0.0",
7677
"y-indexeddb": "^9.0.12",

packages/editor/src/core/components/menus/ai-menu.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { useCallback, useEffect, useRef, useState } from "react";
2-
import tippy, { Instance } from "tippy.js";
2+
import tippy, { type Instance } from "tippy.js";
33
// plane utils
44
import { cn } from "@plane/utils";
55
// types
6-
import { TAIHandler } from "@/types";
6+
import type { TAIHandler } from "@/types";
77

88
type Props = {
99
menu: TAIHandler["menu"];
Lines changed: 127 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
1+
import {
2+
useFloating,
3+
autoUpdate,
4+
offset,
5+
flip,
6+
shift,
7+
useDismiss,
8+
useInteractions,
9+
FloatingPortal,
10+
} from "@floating-ui/react";
111
import type { Editor } from "@tiptap/react";
212
import { Copy, LucideIcon, Trash2 } from "lucide-react";
3-
import { useCallback, useEffect, useRef } from "react";
4-
import tippy, { Instance } from "tippy.js";
13+
import { useCallback, useEffect, useRef, useState } from "react";
14+
import { cn } from "@plane/utils";
515
// constants
616
import { CORE_EXTENSIONS } from "@/constants/extension";
717

@@ -11,67 +21,71 @@ type Props = {
1121

1222
export const BlockMenu = (props: Props) => {
1323
const { editor } = props;
14-
const menuRef = useRef<HTMLDivElement>(null);
15-
const popup = useRef<Instance | null>(null);
16-
17-
const handleClickDragHandle = useCallback((event: MouseEvent) => {
18-
const target = event.target as HTMLElement;
19-
if (target.matches("#drag-handle")) {
20-
event.preventDefault();
21-
22-
popup.current?.setProps({
23-
getReferenceClientRect: () => target.getBoundingClientRect(),
24-
});
25-
26-
popup.current?.show();
27-
return;
28-
}
29-
30-
popup.current?.hide();
31-
return;
32-
}, []);
33-
34-
useEffect(() => {
35-
if (menuRef.current) {
36-
menuRef.current.remove();
37-
menuRef.current.style.visibility = "visible";
38-
39-
// @ts-expect-error - Tippy types are incorrect
40-
popup.current = tippy(document.body, {
41-
getReferenceClientRect: null,
42-
content: menuRef.current,
43-
appendTo: () => document.querySelector(".frame-renderer"),
44-
trigger: "manual",
45-
interactive: true,
46-
arrow: false,
47-
placement: "left-start",
48-
animation: "shift-away",
49-
maxWidth: 500,
50-
hideOnClick: true,
51-
onShown: () => {
52-
menuRef.current?.focus();
53-
},
54-
});
55-
}
56-
57-
return () => {
58-
popup.current?.destroy();
59-
popup.current = null;
60-
};
61-
}, []);
24+
const [isOpen, setIsOpen] = useState(false);
25+
const [isAnimatedIn, setIsAnimatedIn] = useState(false);
26+
const menuRef = useRef<HTMLDivElement | null>(null);
27+
const virtualReferenceRef = useRef<{ getBoundingClientRect: () => DOMRect }>({
28+
getBoundingClientRect: () => new DOMRect(),
29+
});
30+
31+
// Set up Floating UI with virtual reference element
32+
const { refs, floatingStyles, context } = useFloating({
33+
open: isOpen,
34+
onOpenChange: setIsOpen,
35+
middleware: [offset({ crossAxis: -10 }), flip(), shift()],
36+
whileElementsMounted: autoUpdate,
37+
placement: "left-start",
38+
});
39+
40+
const dismiss = useDismiss(context);
41+
const { getFloatingProps } = useInteractions([dismiss]);
42+
43+
// Handle click on drag handle
44+
const handleClickDragHandle = useCallback(
45+
(event: MouseEvent) => {
46+
const target = event.target as HTMLElement;
47+
const dragHandle = target.closest("#drag-handle");
48+
49+
if (dragHandle) {
50+
event.preventDefault();
51+
52+
// Update virtual reference with current drag handle position
53+
virtualReferenceRef.current = {
54+
getBoundingClientRect: () => dragHandle.getBoundingClientRect(),
55+
};
56+
57+
// Set the virtual reference as the reference element
58+
refs.setReference(virtualReferenceRef.current);
59+
60+
// Show the menu
61+
setIsOpen(true);
62+
return;
63+
}
64+
65+
// If clicking outside and not on a menu item, hide the menu
66+
if (menuRef.current && !menuRef.current.contains(target)) {
67+
setIsOpen(false);
68+
}
69+
},
70+
[refs]
71+
);
6272

73+
// Set up event listeners
6374
useEffect(() => {
64-
const handleKeyDown = () => {
65-
popup.current?.hide();
75+
const handleKeyDown = (event: KeyboardEvent) => {
76+
if (event.key === "Escape") {
77+
setIsOpen(false);
78+
}
6679
};
6780

6881
const handleScroll = () => {
69-
popup.current?.hide();
82+
setIsOpen(false);
7083
};
84+
7185
document.addEventListener("click", handleClickDragHandle);
7286
document.addEventListener("contextmenu", handleClickDragHandle);
7387
document.addEventListener("keydown", handleKeyDown);
74-
document.addEventListener("scroll", handleScroll, true); // Using capture phase
88+
document.addEventListener("scroll", handleScroll, true);
7589

7690
return () => {
7791
document.removeEventListener("click", handleClickDragHandle);
@@ -81,6 +95,23 @@ export const BlockMenu = (props: Props) => {
8195
};
8296
}, [handleClickDragHandle]);
8397

98+
// Animation effect
99+
useEffect(() => {
100+
if (isOpen) {
101+
setIsAnimatedIn(false);
102+
// Add a small delay before starting the animation
103+
const timeout = setTimeout(() => {
104+
requestAnimationFrame(() => {
105+
setIsAnimatedIn(true);
106+
});
107+
}, 50);
108+
109+
return () => clearTimeout(timeout);
110+
} else {
111+
setIsAnimatedIn(false);
112+
}
113+
}, [isOpen]);
114+
84115
const MENU_ITEMS: {
85116
icon: LucideIcon;
86117
key: string;
@@ -94,7 +125,7 @@ export const BlockMenu = (props: Props) => {
94125
label: "Delete",
95126
onClick: (e) => {
96127
editor.chain().deleteSelection().focus().run();
97-
popup.current?.hide();
128+
setIsOpen(false);
98129
e.preventDefault();
99130
e.stopPropagation();
100131
},
@@ -143,36 +174,51 @@ export const BlockMenu = (props: Props) => {
143174
console.error(error.message);
144175
}
145176
}
146-
147-
popup.current?.hide();
177+
setIsOpen(false);
148178
},
149179
},
150180
];
151181

152-
return (
153-
<div
154-
ref={menuRef}
155-
className="z-10 max-h-60 min-w-[7rem] overflow-y-scroll rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 shadow-custom-shadow-rg"
156-
>
157-
{MENU_ITEMS.map((item) => {
158-
// Skip rendering the button if it should be disabled
159-
if (item.isDisabled && item.key === "duplicate") {
160-
return null;
161-
}
182+
if (!isOpen) {
183+
return null;
184+
}
162185

163-
return (
164-
<button
165-
key={item.key}
166-
type="button"
167-
className="flex w-full items-center gap-2 truncate rounded px-1 py-1.5 text-xs text-custom-text-200 hover:bg-custom-background-80"
168-
onClick={item.onClick}
169-
disabled={item.isDisabled}
170-
>
171-
<item.icon className="h-3 w-3" />
172-
{item.label}
173-
</button>
174-
);
175-
})}
176-
</div>
186+
return (
187+
<FloatingPortal>
188+
<div
189+
ref={(node) => {
190+
refs.setFloating(node);
191+
menuRef.current = node;
192+
}}
193+
style={{
194+
...floatingStyles,
195+
animationFillMode: "forwards",
196+
transitionTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)", // Expo ease out
197+
}}
198+
className={cn(
199+
"z-20 max-h-60 min-w-[7rem] overflow-y-scroll rounded-lg border border-custom-border-200 bg-custom-background-100 p-1.5 shadow-custom-shadow-rg",
200+
"transition-all duration-300 transform origin-top-right",
201+
isAnimatedIn ? "opacity-100 scale-100" : "opacity-0 scale-75"
202+
)}
203+
{...getFloatingProps()}
204+
>
205+
{MENU_ITEMS.map((item) => {
206+
if (item.isDisabled) return null;
207+
208+
return (
209+
<button
210+
key={item.key}
211+
type="button"
212+
className="flex w-full items-center gap-1.5 truncate rounded px-1 py-1.5 text-xs text-custom-text-200 hover:bg-custom-background-90"
213+
onClick={item.onClick}
214+
disabled={item.isDisabled}
215+
>
216+
<item.icon className="h-3 w-3" />
217+
{item.label}
218+
</button>
219+
);
220+
})}
221+
</div>
222+
</FloatingPortal>
177223
);
178224
};

packages/editor/src/core/extensions/core-without-props.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import TaskItem from "@tiptap/extension-task-item";
22
import TaskList from "@tiptap/extension-task-list";
33
import { TextStyle } from "@tiptap/extension-text-style";
4-
import TiptapUnderline from "@tiptap/extension-underline";
54
import StarterKit from "@tiptap/starter-kit";
65
// helpers
76
import { isValidHttpUrl } from "@/helpers/common";
@@ -76,7 +75,6 @@ export const CoreEditorExtensionsWithoutProps = [
7675
}),
7776
ImageExtensionConfig,
7877
CustomImageExtensionConfig,
79-
TiptapUnderline,
8078
TextStyle,
8179
TaskList.configure({
8280
HTMLAttributes: {

packages/editor/src/core/extensions/custom-image/components/block.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@ import { NodeSelection } from "@tiptap/pm/state";
22
import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from "react";
33
// plane imports
44
import { cn } from "@plane/utils";
5-
// constants
6-
import { CORE_EXTENSIONS } from "@/constants/extension";
7-
// helpers
8-
import { getExtensionStorage } from "@/helpers/get-extension-storage";
95
// local imports
106
import { Pixel, TCustomImageAttributes, TCustomImageSize } from "../types";
117
import { ensurePixelString, getImageBlockId } from "../utils";
@@ -62,7 +58,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
6258
const [hasErroredOnFirstLoad, setHasErroredOnFirstLoad] = useState(false);
6359
const [hasTriedRestoringImageOnce, setHasTriedRestoringImageOnce] = useState(false);
6460
// extension options
65-
const isTouchDevice = !!getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY).isTouchDevice;
61+
const isTouchDevice = !!editor.storage.utility.isTouchDevice;
6662

6763
const updateAttributesSafely = useCallback(
6864
(attributes: Partial<TCustomImageAttributes>, errorMessage: string) => {

packages/editor/src/core/extensions/custom-image/components/uploader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
3939
const { id: imageEntityId } = node.attrs;
4040
// derived values
4141
const imageComponentImageFileMap = useMemo(() => getImageComponentImageFileMap(editor), [editor]);
42-
const isTouchDevice = !!getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY).isTouchDevice;
42+
const isTouchDevice = !!editor.storage.utility.isTouchDevice;
4343

4444
const onUpload = useCallback(
4545
(url: string) => {

packages/editor/src/core/extensions/headings-list.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ export type HeadingExtensionStorage = {
99
headings: IMarking[];
1010
};
1111

12+
declare module "@tiptap/core" {
13+
interface Storage {
14+
[CORE_EXTENSIONS.HEADINGS_LIST]: HeadingExtensionStorage;
15+
}
16+
}
17+
1218
export const HeadingListExtension = Extension.create<unknown, HeadingExtensionStorage>({
1319
name: CORE_EXTENSIONS.HEADINGS_LIST,
1420

@@ -46,6 +52,7 @@ export const HeadingListExtension = Extension.create<unknown, HeadingExtensionSt
4652
this.editor.emit("update", {
4753
editor: this.editor,
4854
transaction: newState.tr,
55+
appendedTransactions: [],
4956
});
5057

5158
return null;

0 commit comments

Comments
 (0)