Skip to content

Commit b38a505

Browse files
vforshVladislav ForshDimillian
authored
fix(composer): make queued message menu work on iOS (#489)
Co-authored-by: Vladislav Forsh <forsh@eot.games> Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
1 parent d22b134 commit b38a505

3 files changed

Lines changed: 125 additions & 32 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/** @vitest-environment jsdom */
2+
import { fireEvent, render, screen } from "@testing-library/react";
3+
import { cleanup } from "@testing-library/react";
4+
import { afterEach, describe, expect, it, vi } from "vitest";
5+
import type { QueuedMessage } from "../../../types";
6+
import { ComposerQueue } from "./ComposerQueue";
7+
8+
const queuedItem: QueuedMessage = {
9+
id: "queued-1",
10+
text: "Add link to GitHub repo too",
11+
createdAt: 1,
12+
};
13+
14+
describe("ComposerQueue", () => {
15+
afterEach(() => {
16+
cleanup();
17+
});
18+
19+
it("opens inline menu on queue item action tap", () => {
20+
render(<ComposerQueue queuedMessages={[queuedItem]} />);
21+
22+
expect(screen.queryByText("Edit")).toBeNull();
23+
fireEvent.click(screen.getByLabelText("Queue item menu"));
24+
expect(screen.getByText("Edit")).toBeTruthy();
25+
expect(screen.getByText("Delete")).toBeTruthy();
26+
});
27+
28+
it("calls edit callback for selected queued item", () => {
29+
const onEditQueued = vi.fn();
30+
render(<ComposerQueue queuedMessages={[queuedItem]} onEditQueued={onEditQueued} />);
31+
32+
fireEvent.click(screen.getByLabelText("Queue item menu"));
33+
fireEvent.click(screen.getByText("Edit"));
34+
35+
expect(onEditQueued).toHaveBeenCalledTimes(1);
36+
expect(onEditQueued).toHaveBeenCalledWith(queuedItem);
37+
});
38+
39+
it("calls delete callback for selected queued item", () => {
40+
const onDeleteQueued = vi.fn();
41+
render(<ComposerQueue queuedMessages={[queuedItem]} onDeleteQueued={onDeleteQueued} />);
42+
43+
fireEvent.click(screen.getByLabelText("Queue item menu"));
44+
fireEvent.click(screen.getByText("Delete"));
45+
46+
expect(onDeleteQueued).toHaveBeenCalledTimes(1);
47+
expect(onDeleteQueued).toHaveBeenCalledWith(queuedItem.id);
48+
});
49+
});
Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { useCallback } from "react";
2-
import { LogicalPosition } from "@tauri-apps/api/dpi";
3-
import { Menu, MenuItem } from "@tauri-apps/api/menu";
4-
import { getCurrentWindow } from "@tauri-apps/api/window";
2+
import type { MouseEvent as ReactMouseEvent } from "react";
53
import type { QueuedMessage } from "../../../types";
4+
import {
5+
PopoverMenuItem,
6+
PopoverSurface,
7+
} from "../../design-system/components/popover/PopoverPrimitives";
8+
import { useMenuController } from "../../app/hooks/useMenuController";
69

710
type ComposerQueueProps = {
811
queuedMessages: QueuedMessage[];
@@ -17,27 +20,6 @@ export function ComposerQueue({
1720
onEditQueued,
1821
onDeleteQueued,
1922
}: ComposerQueueProps) {
20-
const handleQueueMenu = useCallback(
21-
async (event: React.MouseEvent, item: QueuedMessage) => {
22-
event.preventDefault();
23-
event.stopPropagation();
24-
const { clientX, clientY } = event;
25-
const editItem = await MenuItem.new({
26-
text: "Edit",
27-
action: () => onEditQueued?.(item),
28-
});
29-
const deleteItem = await MenuItem.new({
30-
text: "Delete",
31-
action: () => onDeleteQueued?.(item.id),
32-
});
33-
const menu = await Menu.new({ items: [editItem, deleteItem] });
34-
const window = getCurrentWindow();
35-
const position = new LogicalPosition(clientX, clientY);
36-
await menu.popup(position, window);
37-
},
38-
[onDeleteQueued, onEditQueued],
39-
);
40-
4123
if (queuedMessages.length === 0) {
4224
return null;
4325
}
@@ -62,16 +44,63 @@ export function ComposerQueue({
6244
? ` · ${item.images.length} image${item.images.length === 1 ? "" : "s"}`
6345
: ""}
6446
</span>
65-
<button
66-
className="composer-queue-menu"
67-
onClick={(event) => handleQueueMenu(event, item)}
68-
aria-label="Queue item menu"
69-
>
70-
...
71-
</button>
47+
<QueueMenuButton
48+
item={item}
49+
onEditQueued={onEditQueued}
50+
onDeleteQueued={onDeleteQueued}
51+
/>
7252
</div>
7353
))}
7454
</div>
7555
</div>
7656
);
7757
}
58+
59+
type QueueMenuButtonProps = {
60+
item: QueuedMessage;
61+
onEditQueued?: (item: QueuedMessage) => void;
62+
onDeleteQueued?: (id: string) => void;
63+
};
64+
65+
function QueueMenuButton({ item, onEditQueued, onDeleteQueued }: QueueMenuButtonProps) {
66+
const menu = useMenuController();
67+
const handleToggleMenu = useCallback(
68+
(event: ReactMouseEvent<HTMLButtonElement>) => {
69+
event.preventDefault();
70+
event.stopPropagation();
71+
menu.toggle();
72+
},
73+
[menu],
74+
);
75+
76+
const handleEdit = useCallback(() => {
77+
menu.close();
78+
onEditQueued?.(item);
79+
}, [item, menu, onEditQueued]);
80+
81+
const handleDelete = useCallback(() => {
82+
menu.close();
83+
onDeleteQueued?.(item.id);
84+
}, [item.id, menu, onDeleteQueued]);
85+
86+
return (
87+
<div className="composer-queue-menu-wrap" ref={menu.containerRef}>
88+
<button
89+
type="button"
90+
className={`composer-queue-menu${menu.isOpen ? " is-open" : ""}`}
91+
onClick={handleToggleMenu}
92+
aria-label="Queue item menu"
93+
aria-haspopup="menu"
94+
aria-expanded={menu.isOpen}
95+
>
96+
...
97+
</button>
98+
{menu.isOpen && (
99+
<PopoverSurface className="composer-queue-item-popover" role="menu">
100+
<PopoverMenuItem onClick={handleEdit}>Edit</PopoverMenuItem>
101+
<PopoverMenuItem onClick={handleDelete}>Delete</PopoverMenuItem>
102+
</PopoverSurface>
103+
)}
104+
</div>
105+
);
106+
}

src/styles/composer.css

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,25 @@
6666
cursor: pointer;
6767
}
6868

69-
.composer-queue-menu:hover {
69+
.composer-queue-menu-wrap {
70+
position: relative;
71+
flex: 0 0 auto;
72+
}
73+
74+
.composer-queue-menu:hover,
75+
.composer-queue-menu.is-open {
7076
color: var(--text-stronger);
7177
}
7278

79+
.composer-queue-item-popover {
80+
position: absolute;
81+
right: 0;
82+
bottom: calc(100% + 4px);
83+
min-width: 110px;
84+
padding: 4px;
85+
z-index: 40;
86+
}
87+
7388
.composer-followup-hint {
7489
display: flex;
7590
flex-direction: column;

0 commit comments

Comments
 (0)