diff --git a/packages/pdf/src/semantic/item-header-row.test.tsx b/packages/pdf/src/semantic/item-header-row.test.tsx
index 3404f2240..cabc95d8f 100644
--- a/packages/pdf/src/semantic/item-header-row.test.tsx
+++ b/packages/pdf/src/semantic/item-header-row.test.tsx
@@ -62,7 +62,7 @@ const buildFixture = (): ResumeData => {
return data;
};
-const renderTitleRowStyle = async (template: Template, stylesheet?: string) => {
+const renderTitleRowStyles = async (template: Template, stylesheet?: string) => {
const data = buildFixture();
const semanticRuntime = stylesheet
? resolveResumeRuntime({ data, template, mode: "semantic", source: { languageVersion: 1, text: stylesheet } })
@@ -76,9 +76,10 @@ const renderTitleRowStyle = async (template: Template, stylesheet?: string) => {
const path = pathTo(document, (node) => nodeText(node).trim() === LONG_TITLE);
if (!path) throw new Error("Missing certification title in the rendered document");
const row = path.at(-2);
- if (!row) throw new Error("Missing the row wrapping the certification title");
+ const title = path.at(-1);
+ if (!row || !title) throw new Error("Missing the row wrapping the certification title");
- return mergedStyle(row);
+ return { row: mergedStyle(row), title: mergedStyle(title) };
};
describe("item-header-row template part", () => {
@@ -112,8 +113,19 @@ describe("item-header-row template part", () => {
// The row ships with `flex-wrap: wrap`, which drops a long title's date onto its own line. This
// is the whole point of exposing the part, so assert the override reaches the rendered row.
it.each(["onyx", "ditgar", "meowth"] as const)("lets %s stylesheets turn off the row wrap", async (template) => {
- expect(await renderTitleRowStyle(template)).toMatchObject({ flexWrap: "wrap" });
- expect(await renderTitleRowStyle(template, NOWRAP_STYLESHEET)).toMatchObject({ flexWrap: "nowrap" });
+ expect((await renderTitleRowStyles(template)).row).toMatchObject({ flexWrap: "wrap" });
+ expect((await renderTitleRowStyles(template, NOWRAP_STYLESHEET)).row).toMatchObject({ flexWrap: "nowrap" });
+ });
+
+ // React PDF reuses the line layout a Text was first measured with, so a title Yoga shrinks after
+ // that measurement draws its glyphs over the date. Under `nowrap` the title takes a zero flex
+ // basis instead, which makes its first measurement its final width.
+ it("gives the title a zero flex basis only when the row stops wrapping", async () => {
+ expect((await renderTitleRowStyles("onyx")).title).not.toMatchObject({ flexBasis: 0 });
+ expect((await renderTitleRowStyles("onyx", NOWRAP_STYLESHEET)).title).toMatchObject({
+ flexBasis: 0,
+ flexGrow: 1,
+ });
});
it("reports no diagnostics for the selector", () => {
diff --git a/packages/pdf/src/templates/shared/sections.test.ts b/packages/pdf/src/templates/shared/sections.test.ts
index 9430c7cec..7a1a8ea01 100644
--- a/packages/pdf/src/templates/shared/sections.test.ts
+++ b/packages/pdf/src/templates/shared/sections.test.ts
@@ -17,9 +17,8 @@ describe("ExperienceSection", () => {
describe("ItemTitle", () => {
it("renders award titles without the bold style", () => {
expect(source).toContain("const ItemTitle = ({ children, website, field, bold = true }: ItemTitleProps)");
- expect(source).toContain(
- "const title = bold ? {children} : {children};",
- );
+ expect(source).toContain("const title = bold ? (\n\t\t");
+ expect(source).toContain("\t) : (\n\t\t");
expect(source).toContain('');
});
});
diff --git a/packages/pdf/src/templates/shared/sections.tsx b/packages/pdf/src/templates/shared/sections.tsx
index 1becf50af..472c1024f 100644
--- a/packages/pdf/src/templates/shared/sections.tsx
+++ b/packages/pdf/src/templates/shared/sections.tsx
@@ -75,7 +75,7 @@ import { createRtlStyleHelpers } from "./rtl";
import { getInlineItemWebsiteUrl, shouldRenderSeparateItemWebsite } from "./section-links";
import { hasSplitRowText } from "./split-row";
import { getSectionStyleRuleContext } from "./style-rules";
-import { composeStyles } from "./styles";
+import { composeStyles, mergeStyles } from "./styles";
type SectionItemsContextValue = {
itemStyle: StyleInput;
@@ -618,6 +618,18 @@ type ItemHeaderRowProps = {
style: StyleInput;
};
+/**
+ * React PDF lays a `Text` out once, at the width it is first measured at, and reuses those lines
+ * afterwards. `flex-wrap: nowrap` asks Yoga to shrink the title so the date fits beside it, and the
+ * cached lines keep the wider layout, so the title's glyphs run over the date. A zero flex basis
+ * makes the title's first measurement its final width, so it wraps inside its own box instead.
+ */
+const nowrapItemTitleStyle = { flexGrow: 1, flexBasis: 0 } satisfies Style;
+const wrappingItemTitleStyle = {} satisfies Style;
+
+/** True while rendering inside an item header row a stylesheet turned into a single line. */
+const ItemHeaderRowNowrapContext = createContext(false);
+
/**
* The title/date row inside a section item header, exposed to Semantic CSS as
* `template-part[name="item-header-row"]`.
@@ -626,11 +638,20 @@ type ItemHeaderRowProps = {
* the trailing date onto its own line, and `item-header` selects the box around the row rather than
* the row itself. The owning item-header key comes from the surrounding provider.
*/
-const ItemHeaderRow = ({ children, style }: ItemHeaderRowProps) => (
-
- {children}
-
-);
+const ItemHeaderRow = ({ children, style }: ItemHeaderRowProps) => {
+ const ownerNodeKey = useSemanticNodeKey();
+ const resolved = useResolvedNode(semanticTemplatePartNodeKey(ownerNodeKey, ...ITEM_HEADER_ROW_PART_KEYS));
+ // Same order the rendered row composes in, so a template that ships `nowrap` counts too.
+ const { flexWrap } = mergeStyles(style, resolved.style);
+
+ return (
+
+
+ {children}
+
+
+ );
+};
const SectionItemHeader = ({ children }: SectionItemHeaderProps) => {
const itemNodeKey = useSemanticNodeKey();
@@ -687,12 +708,21 @@ const SectionItemHeader = ({ children }: SectionItemHeaderProps) => {
const ItemTitle = ({ children, website, field, bold = true }: ItemTitleProps) => {
const inlineWebsiteUrl = getInlineItemWebsiteUrl(website);
- const title = bold ? {children} : {children};
+ const style = use(ItemHeaderRowNowrapContext) ? nowrapItemTitleStyle : wrappingItemTitleStyle;
+ const title = bold ? (
+
+ {children}
+
+ ) : (
+
+ {children}
+
+ );
if (!inlineWebsiteUrl) return title;
return (
-
+
{title}
);