Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/bundled/github/GitHubPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,51 @@ it("links the PR owner and repository and moves its number to the title", async
).not.toBeInTheDocument();
});

it("opens Command-clicked panel links in the desktop browser", async () => {
const invoke = vi.fn().mockResolvedValue(undefined);
vi.stubGlobal("isTauri", true);
vi.stubGlobal("__TAURI_INTERNALS__", { invoke });
vi.stubGlobal(
"fetch",
vi
.fn()
.mockResolvedValue(
new Response(JSON.stringify({ title: "A small improvement" })),
),
);
const target = "https://github.com/block/buzz-app/pull/629";
render(<GitHubPanel target={target} close={() => {}} />);
const title = await screen.findByRole("link", {
name: "A small improvement #629",
});
const plain = new MouseEvent("click", { bubbles: true, cancelable: true });
fireEvent(title, plain);
// A plain click stays with Tauri's own link opener.
expect(plain.defaultPrevented).toBe(false);
expect(invoke).not.toHaveBeenCalled();
for (const [link, url] of [
[title, target],
[
screen.getByRole("link", { name: "buzz-app" }),
"https://github.com/block/buzz-app",
],
] as const) {
invoke.mockClear();
const command = new MouseEvent("click", {
bubbles: true,
cancelable: true,
metaKey: true,
});
fireEvent(link, command);
expect(command.defaultPrevented).toBe(true);
expect(invoke).toHaveBeenCalledExactlyOnceWith(
"plugin:opener|open_url",
{ url },
undefined,
);
}
});

it.each([
["https://github.com/example/project", "Repository project"],
["https://github.com/example/project/issues/2", "Issue #2"],
Expand Down
27 changes: 26 additions & 1 deletion src/bundled/github/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { invoke, isTauri } from "@tauri-apps/api/core";
import { ResolvePull } from "./ResolvePull";
import type { PullChecklist as Checklist } from "./pull";
import { Button } from "../../shared/design-system/ui/Button";
Expand All @@ -9,6 +10,7 @@ import {
useRef,
useState,
useSyncExternalStore,
type MouseEvent,
type ReactNode,
} from "react";
import {
Expand Down Expand Up @@ -378,6 +380,27 @@ export function GitHubPanel({
);
}

/** Tauri's automatic link opener skips Command-clicks, which left the panel's
* GitHub links doing nothing; send them to the browser as message links do. */
function openCommandClickInBrowser(event: MouseEvent<HTMLElement>) {
if (
event.defaultPrevented ||
!event.metaKey ||
event.button !== 0 ||
!isTauri()
)
return;
const anchor =
event.target instanceof Element ? event.target.closest("a[href]") : null;
if (
!(anchor instanceof HTMLAnchorElement) ||
(anchor.protocol !== "https:" && anchor.protocol !== "http:")
)
return;
event.preventDefault();
void invoke("plugin:opener|open_url", { url: anchor.href }).catch(() => {});
}

function ObjectPanel({
reference,
gh,
Expand Down Expand Up @@ -467,7 +490,9 @@ function ObjectPanel({
</h2>
);
return (
<div className={styles.root}>
// biome-ignore lint/a11y/noStaticElementInteractions: Delegates Command-clicks on the links inside; it adds no interaction of its own.
// biome-ignore lint/a11y/useKeyWithClickEvents: Command-click is a pointer gesture on a link, which keeps its own keyboard behavior.
<div className={styles.root} onClick={openCommandClickInBrowser}>
<div className={styles.identity}>
<span className={styles.icon}>
<Icon size={22} aria-hidden="true" />
Expand Down
Loading