diff --git a/packages/pdf/src/templates/azurill/AzurillPage.tsx b/packages/pdf/src/templates/azurill/AzurillPage.tsx
index f1469c37c..b2b0745fe 100644
--- a/packages/pdf/src/templates/azurill/AzurillPage.tsx
+++ b/packages/pdf/src/templates/azurill/AzurillPage.tsx
@@ -1,5 +1,4 @@
import type { Style } from "@react-pdf/types";
-import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type {
TemplateColorRoles,
@@ -12,6 +11,7 @@ import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { Fragment, useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
+import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -121,17 +121,9 @@ const Header = ({ styles }: { styles: AzurillStyles }) => {
{basics.location}
)}
- {basics.website.url && (
-
-
- {basics.website.label}
-
- )}
+
{basics.customFields.map((field) => (
-
-
- {field.text}
-
+
))}
diff --git a/packages/pdf/src/templates/bronzor/BronzorPage.tsx b/packages/pdf/src/templates/bronzor/BronzorPage.tsx
index d094267e4..fe48cc0fd 100644
--- a/packages/pdf/src/templates/bronzor/BronzorPage.tsx
+++ b/packages/pdf/src/templates/bronzor/BronzorPage.tsx
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
-import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
+import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -109,17 +109,9 @@ const Header = ({ styles }: { styles: BronzorStyles }) => {
{basics.location}
)}
- {basics.website.url && (
-
-
- {basics.website.label}
-
- )}
+
{basics.customFields.map((field) => (
-
-
- {field.text}
-
+
))}
diff --git a/packages/pdf/src/templates/chikorita/ChikoritaPage.tsx b/packages/pdf/src/templates/chikorita/ChikoritaPage.tsx
index 79b1e6009..8ee851f12 100644
--- a/packages/pdf/src/templates/chikorita/ChikoritaPage.tsx
+++ b/packages/pdf/src/templates/chikorita/ChikoritaPage.tsx
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
-import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { Fragment, useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
+import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -127,17 +127,9 @@ const Header = ({ styles }: { styles: ChikoritaStyles }) => {
- {basics.website.url && (
-
-
- {basics.website.label}
-
- )}
+
{basics.customFields.map((field) => (
-
-
- {field.text}
-
+
))}
diff --git a/packages/pdf/src/templates/ditgar/DitgarPage.tsx b/packages/pdf/src/templates/ditgar/DitgarPage.tsx
index 4c85b878b..58091ada3 100644
--- a/packages/pdf/src/templates/ditgar/DitgarPage.tsx
+++ b/packages/pdf/src/templates/ditgar/DitgarPage.tsx
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
-import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateFeatures, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { parseColorString, rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
+import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -129,17 +129,20 @@ const Header = ({ styles, colors }: { styles: DitgarStyles; colors: TemplateColo
{basics.location}
)}
- {basics.website.url && (
-
-
- {basics.website.label}
-
- )}
+
{basics.customFields.map((field) => (
-
-
- {field.text}
-
+
))}
diff --git a/packages/pdf/src/templates/ditto/DittoPage.tsx b/packages/pdf/src/templates/ditto/DittoPage.tsx
index 78bc22d2d..1188f6c0d 100644
--- a/packages/pdf/src/templates/ditto/DittoPage.tsx
+++ b/packages/pdf/src/templates/ditto/DittoPage.tsx
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
-import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { Fragment, useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
+import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -129,17 +129,9 @@ const Header = ({ styles }: { styles: DittoStyles }) => {
{basics.location}
)}
- {basics.website.url && (
-
-
- {basics.website.label}
-
- )}
+
{basics.customFields.map((field) => (
-
-
- {field.text}
-
+
))}
diff --git a/packages/pdf/src/templates/gengar/GengarPage.tsx b/packages/pdf/src/templates/gengar/GengarPage.tsx
index 526a81a64..794aa52a7 100644
--- a/packages/pdf/src/templates/gengar/GengarPage.tsx
+++ b/packages/pdf/src/templates/gengar/GengarPage.tsx
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
-import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { Fragment, useMemo } from "react";
import { parseColorString, rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
+import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -126,17 +126,20 @@ const Header = ({ styles, colors }: { styles: GengarStyles; colors: TemplateColo
{basics.location}
)}
- {basics.website.url && (
-
-
- {basics.website.label}
-
- )}
+
{basics.customFields.map((field) => (
-
-
- {field.text}
-
+
))}
diff --git a/packages/pdf/src/templates/glalie/GlaliePage.tsx b/packages/pdf/src/templates/glalie/GlaliePage.tsx
index 104c48427..2997fabbf 100644
--- a/packages/pdf/src/templates/glalie/GlaliePage.tsx
+++ b/packages/pdf/src/templates/glalie/GlaliePage.tsx
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
-import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateFeatures, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { parseColorString, rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
+import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -124,17 +124,9 @@ const Header = ({ styles }: { styles: GlalieStyles }) => {
{basics.location}
)}
- {basics.website.url && (
-
-
- {basics.website.label}
-
- )}
+
{basics.customFields.map((field) => (
-
-
- {field.text}
-
+
))}
diff --git a/packages/pdf/src/templates/kakuna/KakunaPage.tsx b/packages/pdf/src/templates/kakuna/KakunaPage.tsx
index 5bb9823ca..bc2460670 100644
--- a/packages/pdf/src/templates/kakuna/KakunaPage.tsx
+++ b/packages/pdf/src/templates/kakuna/KakunaPage.tsx
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
-import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
+import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -100,17 +100,9 @@ const Header = ({ styles }: { styles: KakunaStyles }) => {
{basics.location}
)}
- {basics.website.url && (
-
-
- {basics.website.label}
-
- )}
+
{basics.customFields.map((field) => (
-
-
- {field.text}
-
+
))}
diff --git a/packages/pdf/src/templates/lapras/LaprasPage.tsx b/packages/pdf/src/templates/lapras/LaprasPage.tsx
index 85361b00e..122b467fd 100644
--- a/packages/pdf/src/templates/lapras/LaprasPage.tsx
+++ b/packages/pdf/src/templates/lapras/LaprasPage.tsx
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
-import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { parseColorString, rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
+import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -99,17 +99,9 @@ const Header = ({ styles }: { styles: LaprasStyles }) => {
{basics.location}
)}
- {basics.website.url && (
-
-
- {basics.website.label}
-
- )}
+
{basics.customFields.map((field) => (
-
-
- {field.text}
-
+
))}
diff --git a/packages/pdf/src/templates/leafish/LeafishPage.tsx b/packages/pdf/src/templates/leafish/LeafishPage.tsx
index 58ffd10d3..e220e1f7d 100644
--- a/packages/pdf/src/templates/leafish/LeafishPage.tsx
+++ b/packages/pdf/src/templates/leafish/LeafishPage.tsx
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
-import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { parseColorString, rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
+import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -119,17 +119,9 @@ const Header = ({ styles }: { styles: LeafishStyles }) => {
{basics.location}
)}
- {basics.website.url && (
-
-
- {basics.website.label}
-
- )}
+
{basics.customFields.map((field) => (
-
-
- {field.text}
-
+
))}
diff --git a/packages/pdf/src/templates/meowth/MeowthPage.tsx b/packages/pdf/src/templates/meowth/MeowthPage.tsx
index e88441c74..c5bdd80fa 100644
--- a/packages/pdf/src/templates/meowth/MeowthPage.tsx
+++ b/packages/pdf/src/templates/meowth/MeowthPage.tsx
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
-import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateFeatures, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
+import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -102,17 +102,9 @@ const Header = ({ styles }: { styles: MeowthStyles }) => {
{basics.location}
)}
- {basics.website.url && (
-
-
- {basics.website.label}
-
- )}
+
{basics.customFields.map((field) => (
-
-
- {field.text}
-
+
))}
diff --git a/packages/pdf/src/templates/onyx/OnyxPage.tsx b/packages/pdf/src/templates/onyx/OnyxPage.tsx
index 740ee8646..a207a8a8a 100644
--- a/packages/pdf/src/templates/onyx/OnyxPage.tsx
+++ b/packages/pdf/src/templates/onyx/OnyxPage.tsx
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
-import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
+import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -99,17 +99,9 @@ const Header = ({ styles }: { styles: OnyxStyles }) => {
{basics.location}
)}
- {basics.website.url && (
-
-
- {basics.website.label}
-
- )}
+
{basics.customFields.map((field) => (
-
-
- {field.text}
-
+
))}
diff --git a/packages/pdf/src/templates/pikachu/PikachuPage.tsx b/packages/pdf/src/templates/pikachu/PikachuPage.tsx
index f3e1c6d44..417cd7159 100644
--- a/packages/pdf/src/templates/pikachu/PikachuPage.tsx
+++ b/packages/pdf/src/templates/pikachu/PikachuPage.tsx
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
-import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateFeatures, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
+import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -126,17 +126,20 @@ const Header = ({ styles, colors }: { styles: PikachuStyles; colors: TemplateCol
{basics.location}
)}
- {basics.website.url && (
-
-
- {basics.website.label}
-
- )}
+
{basics.customFields.map((field) => (
-
-
- {field.text}
-
+
))}
diff --git a/packages/pdf/src/templates/rhyhorn/RhyhornPage.tsx b/packages/pdf/src/templates/rhyhorn/RhyhornPage.tsx
index 2b40a1789..718ac8d9e 100644
--- a/packages/pdf/src/templates/rhyhorn/RhyhornPage.tsx
+++ b/packages/pdf/src/templates/rhyhorn/RhyhornPage.tsx
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
-import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
+import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -91,16 +91,10 @@ const Header = ({ styles }: { styles: RhyhornStyles }) => {
),
basics.website.url && (
-
-
- {basics.website.label}
-
+
),
...basics.customFields.map((field) => (
-
-
- {field.text}
-
+
)),
].filter(Boolean);
diff --git a/packages/pdf/src/templates/shared/contact-item.tsx b/packages/pdf/src/templates/shared/contact-item.tsx
new file mode 100644
index 000000000..051e1c013
--- /dev/null
+++ b/packages/pdf/src/templates/shared/contact-item.tsx
@@ -0,0 +1,64 @@
+import type { Style } from "@react-pdf/types";
+import type { CustomField } from "@reactive-resume/schema/resume/data";
+import type { IconName } from "phosphor-icons-react-pdf/dynamic";
+import { View } from "@react-pdf/renderer";
+import { getCustomFieldLinkUrl, getWebsiteDisplayText } from "./contact";
+import { Icon, Link, Text } from "./primitives";
+
+type WebsiteDisplay = {
+ url: string;
+ label?: string | undefined;
+};
+
+type ContactStyle = Style | Style[];
+
+export const WebsiteContactItem = ({
+ website,
+ style,
+ textStyle,
+ iconColor,
+}: {
+ website: WebsiteDisplay;
+ style?: ContactStyle;
+ textStyle?: ContactStyle;
+ iconColor?: string;
+}) => {
+ if (!website.url) return null;
+
+ return (
+
+
+ {getWebsiteDisplayText(website)}
+
+ );
+};
+
+export const CustomFieldContactItem = ({
+ field,
+ style,
+ textStyle,
+ iconColor,
+}: {
+ field: CustomField;
+ style?: ContactStyle;
+ textStyle?: ContactStyle;
+ iconColor?: string;
+}) => {
+ const linkUrl = getCustomFieldLinkUrl(field);
+ const children = (
+ <>
+
+ {field.text}
+ >
+ );
+
+ if (linkUrl) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return {children};
+};
diff --git a/packages/pdf/src/templates/shared/contact.test.ts b/packages/pdf/src/templates/shared/contact.test.ts
new file mode 100644
index 000000000..f26762361
--- /dev/null
+++ b/packages/pdf/src/templates/shared/contact.test.ts
@@ -0,0 +1,30 @@
+import { describe, expect, it } from "vitest";
+import { getCustomFieldLinkUrl, getWebsiteDisplayText } from "./contact";
+
+describe("getWebsiteDisplayText", () => {
+ it("falls back to the full URL when the website label is blank", () => {
+ expect(getWebsiteDisplayText({ url: "https://davidkowalski.games", label: "" })).toBe(
+ "https://davidkowalski.games",
+ );
+ expect(getWebsiteDisplayText({ url: "https://davidkowalski.games", label: " " })).toBe(
+ "https://davidkowalski.games",
+ );
+ expect(getWebsiteDisplayText({ url: "http://example.com", label: undefined })).toBe("http://example.com");
+ });
+
+ it("uses the website label when one is provided", () => {
+ expect(getWebsiteDisplayText({ url: "https://davidkowalski.games", label: "Portfolio" })).toBe("Portfolio");
+ });
+});
+
+describe("getCustomFieldLinkUrl", () => {
+ it("returns no link URL for blank custom field links", () => {
+ expect(getCustomFieldLinkUrl({ link: "" })).toBeUndefined();
+ expect(getCustomFieldLinkUrl({ link: " " })).toBeUndefined();
+ expect(getCustomFieldLinkUrl({ link: undefined })).toBeUndefined();
+ });
+
+ it("returns the custom field link URL when one is provided", () => {
+ expect(getCustomFieldLinkUrl({ link: "https://example.com" })).toBe("https://example.com");
+ });
+});
diff --git a/packages/pdf/src/templates/shared/contact.ts b/packages/pdf/src/templates/shared/contact.ts
new file mode 100644
index 000000000..e960f315c
--- /dev/null
+++ b/packages/pdf/src/templates/shared/contact.ts
@@ -0,0 +1,20 @@
+type WebsiteDisplay = {
+ url: string;
+ label?: string | undefined;
+};
+
+type CustomFieldLink = {
+ link?: string | undefined;
+};
+
+export const getWebsiteDisplayText = (website: WebsiteDisplay): string => {
+ const label = website.label?.trim();
+
+ return label || website.url;
+};
+
+export const getCustomFieldLinkUrl = (field: CustomFieldLink): string | undefined => {
+ const link = field.link?.trim();
+
+ return link || undefined;
+};
diff --git a/packages/pdf/src/templates/shared/sections.tsx b/packages/pdf/src/templates/shared/sections.tsx
index e6cc889da..fe85516af 100644
--- a/packages/pdf/src/templates/shared/sections.tsx
+++ b/packages/pdf/src/templates/shared/sections.tsx
@@ -25,6 +25,7 @@ import { match } from "ts-pattern";
import { useRender } from "../../context";
import { getResumeSectionTitle } from "../../section-title";
import { getSectionItemRows, getSectionItemsLayout, shouldUseSectionTimeline } from "./columns";
+import { getWebsiteDisplayText } from "./contact";
import {
TemplatePlacementProvider,
useTemplateFeature,
@@ -225,7 +226,7 @@ const ItemTitle = ({ children, website }: { children: ReactNode; website: ItemWe
const ItemWebsiteLink = ({ website }: { website: ItemWebsite }) => {
if (!shouldRenderSeparateItemWebsite(website)) return null;
- return {website.label || website.url};
+ return {getWebsiteDisplayText(website)};
};
const SummarySection = ({ showHeading = true }: { showHeading?: boolean } = {}) => {
diff --git a/packages/ui/src/components/input-group.test.tsx b/packages/ui/src/components/input-group.test.tsx
new file mode 100644
index 000000000..c3e46dd98
--- /dev/null
+++ b/packages/ui/src/components/input-group.test.tsx
@@ -0,0 +1,34 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, test } from "vitest";
+import { createPortal } from "react-dom";
+import { InputGroup, InputGroupAddon, InputGroupInput } from "./input-group";
+
+function PortaledInput() {
+ return createPortal(, document.body);
+}
+
+describe("InputGroupAddon", () => {
+ test("does not redirect focus from controls rendered through a portal", async () => {
+ const user = userEvent.setup();
+
+ render(
+
+
+ Addon
+
+
+
+
+ ,
+ );
+
+ const portaledInput = screen.getByLabelText("Portaled input");
+ const outerInput = screen.getByLabelText("Outer input");
+
+ await user.click(portaledInput);
+
+ expect(document.activeElement).toBe(portaledInput);
+ expect(document.activeElement).not.toBe(outerInput);
+ });
+});
diff --git a/packages/ui/src/components/input-group.tsx b/packages/ui/src/components/input-group.tsx
index 9185e37d8..4e58b12a1 100644
--- a/packages/ui/src/components/input-group.tsx
+++ b/packages/ui/src/components/input-group.tsx
@@ -48,6 +48,7 @@ function InputGroupAddon({
data-slot="input-group-addon"
className={cn(inputGroupAddonVariants({ align }), className)}
onKeyDown={(e) => {
+ if (!(e.target instanceof Element) || !e.currentTarget.contains(e.target)) return;
// Only respond to Space or Enter
if (e.key !== " " && e.key !== "Enter") return;
if (!(e.target as HTMLElement).closest("button")) {
@@ -56,6 +57,7 @@ function InputGroupAddon({
}
}}
onClick={(e) => {
+ if (!(e.target instanceof Element) || !e.currentTarget.contains(e.target)) return;
if (!(e.target as HTMLElement).closest("button")) {
e.preventDefault();
e.currentTarget.parentElement?.querySelector("input")?.focus();