Skip to content

Commit 12c46e2

Browse files
authored
Merge pull request #194 from ReScienceLab/radix-menus
Menus and the device filter on Radix
2 parents c142ec2 + 791ba83 commit 12c46e2

8 files changed

Lines changed: 616 additions & 706 deletions

File tree

‎canvas/src/CanvasStrip.tsx‎

Lines changed: 74 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@ import { canvasIndex, LAYOUT_CHANGED } from "./canvasIndex";
66
import { groundEditable, setGround, useGround } from "./canvasGround";
77
import { ViewIcon } from "./CanvasTabBar";
88
import { shareUrl, sheetPageUrl, type CanvasTab } from "./canvasUrl";
9-
import { confirmTrash, openMenu, REVEAL, TRASH } from "./contextMenu";
9+
import {
10+
confirmTrash,
11+
MenuItem,
12+
MenuSeparator,
13+
REVEAL,
14+
RightClickMenu,
15+
TRASH,
16+
} from "./contextMenu";
1017
import { DocModeSwitch } from "./DocTab";
1118
import { FileText, LogoFigma, Plus } from "./geistIcons";
1219

@@ -39,8 +46,6 @@ export function CanvasStrip() {
3946
// Keyed by the page. The project's own view with no canvas has none, and the default ground.
4047
const page = activeTab.kind === "canvas" ? pageOf(activeTab) : undefined;
4148
const ground = useGround(editor, page);
42-
const [target, setTarget] = useState<CanvasTab | null>(null);
43-
const menu = useRef<HTMLDivElement>(null);
4449
const [renaming, setRenaming] = useState(() => sessionStorage.getItem(RENAME_KEY));
4550
// A name typed lands as a layout change, which the tab reads through the index.
4651
const [, relabel] = useReducer((n: number) => n + 1, 0);
@@ -62,7 +67,6 @@ export function CanvasStrip() {
6267
if (editor && made.current) openTab({ kind: "canvas", slug: made.current });
6368
if (editor) made.current = null;
6469
}, [editor, openTab]);
65-
const own = target?.kind === "canvas" && tab.kind !== "example" && canvasIndex().served;
6670
const folder = async (canvas: string, action: "reveal" | "delete") => {
6771
const response = await fetch(`${import.meta.env.BASE_URL}__sp/canvas-folder`, {
6872
method: "POST",
@@ -72,6 +76,45 @@ export function CanvasStrip() {
7276
if (!response.ok) return alert(await response.text());
7377
if (action === "delete") window.location.reload();
7478
};
79+
// Right-click menu: Copy link copies the window's address for that view. A canvas of the
80+
// project's own also shows its folder, or bins it after asking.
81+
const menuFor = (target: CanvasTab) => (
82+
<>
83+
<MenuItem
84+
onSelect={() =>
85+
navigator.clipboard.writeText(
86+
shareUrl(
87+
new URL(tabUrl({ ...tab, view: target }), location.href).href,
88+
canvasIndex().title,
89+
),
90+
)
91+
}
92+
>
93+
Copy link
94+
</MenuItem>
95+
{target.kind === "canvas" && tab.kind !== "example" && canvasIndex().served && (
96+
<>
97+
<MenuItem onSelect={() => folder(target.slug, "reveal")}>{REVEAL}</MenuItem>
98+
<MenuSeparator />
99+
<MenuItem
100+
className="sp-context-menu__danger"
101+
onSelect={async () => {
102+
if (
103+
await confirmTrash(
104+
shortName(target.slug),
105+
`${canvasIndex().canvasesDir}/${target.slug}`,
106+
"Its boards and everything pasted on it go with it.",
107+
)
108+
)
109+
void folder(target.slug, "delete");
110+
}}
111+
>
112+
{TRASH}
113+
</MenuItem>
114+
</>
115+
)}
116+
</>
117+
);
75118
const rename = (canvas: string, name: string) => {
76119
sessionStorage.removeItem(RENAME_KEY);
77120
setRenaming(null);
@@ -86,18 +129,18 @@ export function CanvasStrip() {
86129
return (
87130
<nav className="sp-canvas-tabs" aria-label="Canvases">
88131
{docsOf(tab).map(({ name, slug }) => (
89-
<button
90-
key={slug}
91-
type="button"
92-
className="sp-canvas-tab"
93-
aria-current={activeTab.kind === "doc" && activeTab.slug === slug ? "page" : undefined}
94-
title={name}
95-
onClick={() => openTab({ kind: "doc", slug })}
96-
onContextMenu={(event) => openMenu(event, menu, () => setTarget({ kind: "doc", slug }))}
97-
>
98-
<FileText />
99-
{name.replace(/\.md$/i, "")}
100-
</button>
132+
<RightClickMenu key={slug} menu={menuFor({ kind: "doc", slug })}>
133+
<button
134+
type="button"
135+
className="sp-canvas-tab"
136+
aria-current={activeTab.kind === "doc" && activeTab.slug === slug ? "page" : undefined}
137+
title={name}
138+
onClick={() => openTab({ kind: "doc", slug })}
139+
>
140+
<FileText />
141+
{name.replace(/\.md$/i, "")}
142+
</button>
143+
</RightClickMenu>
101144
))}
102145
{canvases.map((canvas) =>
103146
canvas === renaming ? (
@@ -121,23 +164,21 @@ export function CanvasStrip() {
121164
/>
122165
</label>
123166
) : (
124-
<button
125-
key={canvas}
126-
type="button"
127-
className="sp-canvas-tab"
128-
aria-current={canvas === here ? "page" : undefined}
129-
data-working={busy.includes(canvas) || undefined}
130-
onClick={() => openTab({ kind: "canvas", slug: canvas })}
131-
onDoubleClick={() =>
132-
tab.kind !== "example" && canvasIndex().served && setRenaming(canvas)
133-
}
134-
onContextMenu={(event) =>
135-
openMenu(event, menu, () => setTarget({ kind: "canvas", slug: canvas }))
136-
}
137-
>
138-
<ViewIcon view={{ kind: "canvas", slug: canvas }} />
139-
{shortName(canvas)}
140-
</button>
167+
<RightClickMenu key={canvas} menu={menuFor({ kind: "canvas", slug: canvas })}>
168+
<button
169+
type="button"
170+
className="sp-canvas-tab"
171+
aria-current={canvas === here ? "page" : undefined}
172+
data-working={busy.includes(canvas) || undefined}
173+
onClick={() => openTab({ kind: "canvas", slug: canvas })}
174+
onDoubleClick={() =>
175+
tab.kind !== "example" && canvasIndex().served && setRenaming(canvas)
176+
}
177+
>
178+
<ViewIcon view={{ kind: "canvas", slug: canvas }} />
179+
{shortName(canvas)}
180+
</button>
181+
</RightClickMenu>
141182
),
142183
)}
143184
{tab.kind !== "example" && canvasIndex().served && (
@@ -186,61 +227,6 @@ export function CanvasStrip() {
186227
<LogoFigma />
187228
</a>
188229
)}
189-
{/* Right-click menu: Copy link copies the window's address for that view. A canvas of the
190-
project's own also shows its folder, or bins it after asking. */}
191-
<div
192-
ref={menu}
193-
popover="auto"
194-
className="sp-context-menu"
195-
role="menu"
196-
onClickCapture={(event) => event.currentTarget.hidePopover()}
197-
>
198-
<button
199-
type="button"
200-
role="menuitem"
201-
className="sp-menu-row"
202-
onClick={() =>
203-
navigator.clipboard.writeText(
204-
shareUrl(
205-
new URL(tabUrl({ ...tab, view: target! }), location.href).href,
206-
canvasIndex().title,
207-
),
208-
)
209-
}
210-
>
211-
Copy link
212-
</button>
213-
{own && (
214-
<>
215-
<button
216-
type="button"
217-
role="menuitem"
218-
className="sp-menu-row"
219-
onClick={() => folder(target.slug, "reveal")}
220-
>
221-
{REVEAL}
222-
</button>
223-
<hr />
224-
<button
225-
type="button"
226-
role="menuitem"
227-
className="sp-menu-row sp-context-menu__danger"
228-
onClick={async () => {
229-
if (
230-
await confirmTrash(
231-
shortName(target.slug),
232-
`${canvasIndex().canvasesDir}/${target.slug}`,
233-
"Its boards and everything pasted on it go with it.",
234-
)
235-
)
236-
void folder(target.slug, "delete");
237-
}}
238-
>
239-
{TRASH}
240-
</button>
241-
</>
242-
)}
243-
</div>
244230
</nav>
245231
);
246232
}

0 commit comments

Comments
 (0)