Skip to content
Open
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
6 changes: 3 additions & 3 deletions packages/views/layout/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,7 @@ export function AppSidebar({ topSlot, searchSlot, headerClassName, headerStyle }
<SidebarMenuItem key={item.key}>
<SidebarMenuButton
isActive={isActive}
render={<AppLink href={href} />}
render={<AppLink href={href} activateTabOnClick />}
className="text-muted-foreground hover:not-data-active:bg-sidebar-accent/70 data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground"
>
<item.icon />
Expand Down Expand Up @@ -690,7 +690,7 @@ export function AppSidebar({ topSlot, searchSlot, headerClassName, headerStyle }
<SidebarMenuItem key={item.key}>
<SidebarMenuButton
isActive={isActive}
render={<AppLink href={href} />}
render={<AppLink href={href} activateTabOnClick />}
className="text-muted-foreground hover:not-data-active:bg-sidebar-accent/70 data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground"
>
<item.icon />
Expand All @@ -714,7 +714,7 @@ export function AppSidebar({ topSlot, searchSlot, headerClassName, headerStyle }
<SidebarMenuItem key={item.key}>
<SidebarMenuButton
isActive={isActive}
render={<AppLink href={href} />}
render={<AppLink href={href} activateTabOnClick />}
className="text-muted-foreground hover:not-data-active:bg-sidebar-accent/70 data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground"
>
<item.icon />
Expand Down
24 changes: 24 additions & 0 deletions packages/views/navigation/app-link.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,30 @@ describe("AppLink", () => {
expect(push).not.toHaveBeenCalled();
});

it("activateTabOnClick delegates normal clicks to openInNewTab in the foreground", () => {
const push = vi.fn();
const openInNewTab = vi.fn();
const adapter = makeAdapter({ push, openInNewTab });

renderLink(adapter, { href: "/issues", activateTabOnClick: true });
fireEvent.click(screen.getByText("go"));

expect(openInNewTab).toHaveBeenCalledWith("/issues", undefined, {
activate: true,
});
expect(push).not.toHaveBeenCalled();
});

it("activateTabOnClick falls back to push when the adapter has no tab API", () => {
const push = vi.fn();
const adapter = makeAdapter({ push });

renderLink(adapter, { href: "/issues", activateTabOnClick: true });
fireEvent.click(screen.getByText("go"));

expect(push).toHaveBeenCalledWith("/issues");
});

it("a caller-supplied onClick passed via spread cannot silently override the navigation handler", () => {
const push = vi.fn();
const adapter = makeAdapter({ push });
Expand Down
8 changes: 7 additions & 1 deletion packages/views/navigation/app-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { useNavigation } from "./context";

interface AppLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
href: string;
/** Desktop tab shells can opt into open-or-activate semantics for primary clicks. */
activateTabOnClick?: boolean;
}

export const AppLink = forwardRef<HTMLAnchorElement, AppLinkProps>(
function AppLink(
{ href, children, onClick, onMouseEnter, onFocus, ...props },
{ href, children, onClick, onMouseEnter, onFocus, activateTabOnClick, ...props },
ref,
) {
const { push, openInNewTab, prefetch } = useNavigation();
Expand All @@ -27,6 +29,10 @@ export const AppLink = forwardRef<HTMLAnchorElement, AppLinkProps>(
// (close popover, clear selection, blur the trigger) lands in the
// same tick rather than getting deferred behind the transition.
onClick?.(e);
if (activateTabOnClick && openInNewTab) {
openInNewTab(href, undefined, { activate: true });
return;
}
push(href);
};

Expand Down
Loading