Skip to content
30 changes: 16 additions & 14 deletions apps/www/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,24 @@ a[data-external-link]:not(:has(img)):not(.fd-card):not([data-card]):not(
border-bottom-width: 2.5px;
}

/* External links in documentation */
article
a[href^="http"]:not([href*="arkenv.js.org"]):not([href*="localhost"]):not(
[data-external-link]
):not(.fd-card):not([data-card]):not([class*="fd-"]):not(
a[data-external-link]:not(.fd-card):not([data-card]):not([class*="fd-"]):not(
[data-no-arrow]
) {
padding-right: 1.1em;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23374151' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='7 7 17 7 17 17'%3E%3C/polyline%3E%3Cline x1='7' y1='17' x2='17' y2='7'%3E%3C/line%3E%3C/svg%3E");
background-repeat: no-repeat;
background-size: 0.9em;
background-position: center right;
}

.dark
article
a[data-external-link]:not(.fd-card):not([data-card]):not([class*="fd-"]):not(
[data-no-arrow]
)::after {
content: "";
display: inline-block;
width: 0.9em;
height: 0.9em;
margin-left: 0.125rem;
background-color: currentColor;
-webkit-mask: var(--external-link-arrow) no-repeat center / contain;
mask: var(--external-link-arrow) no-repeat center / contain;
opacity: 0.7;
vertical-align: middle;
) {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23d1d5db' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='7 7 17 7 17 17'%3E%3C/polyline%3E%3Cline x1='7' y1='17' x2='17' y2='7'%3E%3C/line%3E%3C/svg%3E");
}

button[data-search-full] {
Expand Down
34 changes: 17 additions & 17 deletions apps/www/components/ui/external-link.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,41 +20,41 @@ describe("ExternalLink", () => {
);
const link = screen.getByText("External Link");
expect(link).toBeInTheDocument();
// Check for arrow icon by checking SVG is present
const svg = link.closest("a")?.querySelector("svg");
expect(svg).toBeInTheDocument();
// Check for arrow icon by checking data-external-link attribute is present
const a = link.closest("a");
expect(a).toHaveAttribute("data-external-link", "true");
});

it("should render arrow icon for external HTTPS links", () => {
render(
<ExternalLink href="https://arktype.io">ArkType Docs</ExternalLink>,
);
const link = screen.getByText("ArkType Docs");
const svg = link.closest("a")?.querySelector("svg");
expect(svg).toBeInTheDocument();
const a = link.closest("a");
expect(a).toHaveAttribute("data-external-link", "true");
});

it("should NOT render arrow icon for internal relative links", () => {
render(
<ExternalLink href="/docs/quickstart">Internal Link</ExternalLink>,
);
const link = screen.getByText("Internal Link");
const svg = link.closest("a")?.querySelector("svg");
expect(svg).not.toBeInTheDocument();
const a = link.closest("a");
expect(a).not.toHaveAttribute("data-external-link");
});

it("should NOT render arrow icon for hash links", () => {
render(<ExternalLink href="#section">Hash Link</ExternalLink>);
const link = screen.getByText("Hash Link");
const svg = link.closest("a")?.querySelector("svg");
expect(svg).not.toBeInTheDocument();
const a = link.closest("a");
expect(a).not.toHaveAttribute("data-external-link");
});

it("should NOT render arrow icon when href is undefined", () => {
render(<ExternalLink>No Href Link</ExternalLink>);
const link = screen.getByText("No Href Link");
const svg = link.closest("a")?.querySelector("svg");
expect(svg).not.toBeInTheDocument();
const a = link.closest("a");
expect(a).not.toHaveAttribute("data-external-link");
});

it("should NOT render arrow icon for arkenv.js.org links (same domain)", () => {
Expand All @@ -64,8 +64,8 @@ describe("ExternalLink", () => {
</ExternalLink>,
);
const link = screen.getByText("Same Domain Link");
const svg = link.closest("a")?.querySelector("svg");
expect(svg).not.toBeInTheDocument();
const a = link.closest("a");
expect(a).not.toHaveAttribute("data-external-link");
});

it("should NOT render arrow icon for localhost links", () => {
Expand All @@ -75,8 +75,8 @@ describe("ExternalLink", () => {
</ExternalLink>,
);
const link = screen.getByText("Localhost Link");
const svg = link.closest("a")?.querySelector("svg");
expect(svg).not.toBeInTheDocument();
const a = link.closest("a");
expect(a).not.toHaveAttribute("data-external-link");
});
});

Expand All @@ -102,13 +102,13 @@ describe("ExternalLink", () => {
});

describe("accessibility", () => {
it("should render arrow icon that is hidden from screen readers", () => {
it("should NOT render an SVG icon (it is decorative CSS background)", () => {
render(
<ExternalLink href="https://example.com">External Link</ExternalLink>,
);
const link = screen.getByText("External Link").closest("a");
const svg = link?.querySelector("svg");
expect(svg).toHaveAttribute("aria-hidden", "true");
expect(svg).not.toBeInTheDocument();
});
});
});
8 changes: 0 additions & 8 deletions apps/www/components/ui/external-link.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

import FumadocsLink from "fumadocs-core/link";
import { ArrowUpRight } from "lucide-react";
import type { ComponentProps, FC } from "react";
import { isExternalUrl } from "~/lib/utils/url";

Expand All @@ -27,13 +26,6 @@ export const ExternalLink: FC<ExternalLinkProps> = ({
{...props}
>
{children}
{isExternal && (
<ArrowUpRight
className="inline align-middle h-[0.9em] w-[0.9em] opacity-70 ml-0.5"
stroke="currentColor"
aria-hidden="true"
/>
)}
</FumadocsLink>
);
};
Loading