Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Link from "next/link";
import { IntentLink } from "@/features/shared/intent-link";

export interface BreadcrumbItem {
name: string;
Expand Down Expand Up @@ -34,9 +34,9 @@ export function EntryPageBreadcrumb({ items }: Props) {
</span>
) : (
<>
<Link href={item.path} className="hover:underline whitespace-nowrap">
<IntentLink href={item.path} className="hover:underline whitespace-nowrap">
{item.name}
</Link>
</IntentLink>
<span aria-hidden="true">/</span>
</>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Button } from "@/features/ui";
import { UilBars } from "@tooni/iconscout-unicons-react";
import i18next from "i18next";
import Image from "next/image";
import Link from "next/link";
import { IntentLink } from "@/features/shared/intent-link";
import defaults from "@/defaults";

interface Props {
Expand All @@ -13,7 +13,7 @@ export function NavbarMainSidebarToggle({ onClick }: Props) {
return (
<div className="h-[40px] min-w-[40px] md:min-w-[60px] flex items-center gap-1.5 cursor-pointer relative">
<Button onClick={onClick} appearance="gray-link" noPadding={true} icon={<UilBars />} aria-label={i18next.t("navbar.toggle-menu")} />
<Link className="hidden md:block" href="/">
<IntentLink className="hidden md:block" href="/">
{/*
priority: the logo is always above the fold; the default-lazy
behavior deferred its request until the first layout finished
Expand All @@ -28,7 +28,7 @@ export function NavbarMainSidebarToggle({ onClick }: Props) {
height={40}
priority
/>
</Link>
</IntentLink>
</div>
);
}
6 changes: 3 additions & 3 deletions apps/web/src/features/shared/navbar/navbar-mobile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { Button } from "@ui/button";
import clsx from "clsx";
import i18next from "i18next";
import Image from "next/image";
import Link from "next/link";
import { IntentLink } from "@/features/shared/intent-link";
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import defaults from "@/defaults";
Expand Down Expand Up @@ -132,11 +132,11 @@ export function NavbarMobile({
aria-label={i18next.t("navbar.toggle-menu")}
aria-expanded={mainBarExpanded}
/>
<Link href="/" className="flex items-center">
<IntentLink href="/" className="flex items-center">
{/* The image alt provides the link's accessible name (brand/home),
distinct from the "Home" feed tab below. */}
<Image src={defaults.logo} alt="Ecency" width={30} height={30} className="rounded" />
</Link>
</IntentLink>
</div>
<Button
className="ml-auto"
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/features/shared/navbar/navbar-text-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import React, { Fragment } from "react";
import Link from "next/link";
import { IntentLink } from "@/features/shared/intent-link";
import i18next from "i18next";
import i18n from "i18next";
import { EcencyConfigManager } from "@/config";
Expand Down Expand Up @@ -50,7 +50,7 @@ export function NavbarTextMenu() {
<div className="hidden sm:flex md:hidden xl:flex text-menu items-center gap-4 justify-center h-full md:mr-2">
{visibleItems.map((item, i) => (
<Fragment key={i}>
<Link
<IntentLink
key={item.link}
className={classNameObject({
"text-sm font-semibold duration-300 hover:opacity-75 mt-0 px-2 py-0.5 rounded-2xl":
Expand All @@ -63,7 +63,7 @@ export function NavbarTextMenu() {
href={item.link}
>
{item.label}
</Link>
</IntentLink>
{i !== visibleItems.length - 1 && (
<i
key={"circle" + item.label}
Expand Down
9 changes: 6 additions & 3 deletions apps/web/src/features/ui/button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import React, { forwardRef } from "react";
import { ButtonProps } from "./props";
import { classNameObject, useFilteredProps } from "@/features/ui/util";
import { BUTTON_OUTLINE_STYLES, BUTTON_SIZES, BUTTON_STYLES } from "@/features/ui/button/styles";
import Link from "next/link";
// IntentLink prefetches on hover/touch/focus instead of viewport entry:
// every visible href Button used to fire an RSC prefetch + origin render
// just by scrolling into view (#1666).
import { IntentLink } from "@/features/shared/intent-link";
import { UilSpinner } from "@tooni/iconscout-unicons-react";

export * from "./props";
Expand Down Expand Up @@ -86,10 +89,10 @@ const ForwardedButton = forwardRef<HTMLButtonElement | HTMLAnchorElement, Button
const children = props.children ? <div>{props.children}</div> : <></>;

return "href" in props ? (
<Link {...nativeProps} className={className} ref={ref as any}>
<IntentLink {...nativeProps} className={className} ref={ref as React.Ref<HTMLAnchorElement>}>
{props.isLoading && props.loadingText ? props.loadingText : children}
{icon}
</Link>
</IntentLink>
) : (
<button
{...nativeProps}
Expand Down
53 changes: 53 additions & 0 deletions apps/web/src/specs/features/ui/button.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,56 @@ describe("Button", () => {
});
});
});

describe("Button href prefetch behavior (#1666)", () => {
/*
A Button with href used to render a raw next/link, which fires an RSC
prefetch plus an origin render the moment it scrolls into view. The href
branch now renders IntentLink: no prefetch on render, prefetch on intent.
*/
const makeRouter = () => ({
push: vi.fn(),
replace: vi.fn(),
refresh: vi.fn(),
back: vi.fn(),
forward: vi.fn(),
prefetch: vi.fn()
});

const renderWithRouter = async (ui: React.ReactElement, router: ReturnType<typeof makeRouter>) => {
const { AppRouterContext } = await import(
"next/dist/shared/lib/app-router-context.shared-runtime"
);
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
return render(
<AppRouterContext.Provider value={router as never}>{ui}</AppRouterContext.Provider>
);
};

it("does not prefetch on render", async () => {
const router = makeRouter();
await renderWithRouter(<Button href="/perks">Perks</Button>, router);
expect(router.prefetch).not.toHaveBeenCalled();
});

it("prefetches on hover intent", async () => {
const router = makeRouter();
await renderWithRouter(<Button href="/perks">Perks</Button>, router);
fireEvent.mouseEnter(screen.getByText("Perks").closest("a")!);
expect(router.prefetch).toHaveBeenCalledWith("/perks", expect.anything());
});
});

describe("Button href ref forwarding", () => {
it("forwards the ref through IntentLink to the anchor element", () => {
// React 19 passes ref as a prop through function components; IntentLink
// spreads it onto next/link, which attaches it to the rendered <a>.
const ref = React.createRef<HTMLAnchorElement>();
render(
<Button href="/perks" ref={ref as React.Ref<HTMLButtonElement | HTMLAnchorElement>}>
Perks
</Button>
);
expect(ref.current).toBeInstanceOf(HTMLAnchorElement);
expect(ref.current?.getAttribute("href")).toBe("/perks");
});
});
Loading