mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-24 07:12:18 +10:00
fix(stylesheet): stop item header titles overlapping the date under nowrap (#3355)
This commit is contained in:
@@ -62,7 +62,7 @@ const buildFixture = (): ResumeData => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTitleRowStyle = async (template: Template, stylesheet?: string) => {
|
const renderTitleRowStyles = async (template: Template, stylesheet?: string) => {
|
||||||
const data = buildFixture();
|
const data = buildFixture();
|
||||||
const semanticRuntime = stylesheet
|
const semanticRuntime = stylesheet
|
||||||
? resolveResumeRuntime({ data, template, mode: "semantic", source: { languageVersion: 1, text: 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);
|
const path = pathTo(document, (node) => nodeText(node).trim() === LONG_TITLE);
|
||||||
if (!path) throw new Error("Missing certification title in the rendered document");
|
if (!path) throw new Error("Missing certification title in the rendered document");
|
||||||
const row = path.at(-2);
|
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", () => {
|
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
|
// 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.
|
// 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) => {
|
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 renderTitleRowStyles(template)).row).toMatchObject({ flexWrap: "wrap" });
|
||||||
expect(await renderTitleRowStyle(template, NOWRAP_STYLESHEET)).toMatchObject({ flexWrap: "nowrap" });
|
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", () => {
|
it("reports no diagnostics for the selector", () => {
|
||||||
|
|||||||
@@ -17,9 +17,8 @@ describe("ExperienceSection", () => {
|
|||||||
describe("ItemTitle", () => {
|
describe("ItemTitle", () => {
|
||||||
it("renders award titles without the bold style", () => {
|
it("renders award titles without the bold style", () => {
|
||||||
expect(source).toContain("const ItemTitle = ({ children, website, field, bold = true }: ItemTitleProps)");
|
expect(source).toContain("const ItemTitle = ({ children, website, field, bold = true }: ItemTitleProps)");
|
||||||
expect(source).toContain(
|
expect(source).toContain("const title = bold ? (\n\t\t<Bold style={style} semanticField={field}>");
|
||||||
"const title = bold ? <Bold semanticField={field}>{children}</Bold> : <Text semanticField={field}>{children}</Text>;",
|
expect(source).toContain("\t) : (\n\t\t<Text style={style} semanticField={field}>");
|
||||||
);
|
|
||||||
expect(source).toContain('<ItemTitle field="title" website={item.website} bold={false}>');
|
expect(source).toContain('<ItemTitle field="title" website={item.website} bold={false}>');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ import { createRtlStyleHelpers } from "./rtl";
|
|||||||
import { getInlineItemWebsiteUrl, shouldRenderSeparateItemWebsite } from "./section-links";
|
import { getInlineItemWebsiteUrl, shouldRenderSeparateItemWebsite } from "./section-links";
|
||||||
import { hasSplitRowText } from "./split-row";
|
import { hasSplitRowText } from "./split-row";
|
||||||
import { getSectionStyleRuleContext } from "./style-rules";
|
import { getSectionStyleRuleContext } from "./style-rules";
|
||||||
import { composeStyles } from "./styles";
|
import { composeStyles, mergeStyles } from "./styles";
|
||||||
|
|
||||||
type SectionItemsContextValue = {
|
type SectionItemsContextValue = {
|
||||||
itemStyle: StyleInput;
|
itemStyle: StyleInput;
|
||||||
@@ -618,6 +618,18 @@ type ItemHeaderRowProps = {
|
|||||||
style: StyleInput;
|
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
|
* The title/date row inside a section item header, exposed to Semantic CSS as
|
||||||
* `template-part[name="item-header-row"]`.
|
* `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 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.
|
* the row itself. The owning item-header key comes from the surrounding provider.
|
||||||
*/
|
*/
|
||||||
const ItemHeaderRow = ({ children, style }: ItemHeaderRowProps) => (
|
const ItemHeaderRow = ({ children, style }: ItemHeaderRowProps) => {
|
||||||
<SemanticTemplatePartView partKeys={ITEM_HEADER_ROW_PART_KEYS} style={composeStyles(style)}>
|
const ownerNodeKey = useSemanticNodeKey();
|
||||||
{children}
|
const resolved = useResolvedNode(semanticTemplatePartNodeKey(ownerNodeKey, ...ITEM_HEADER_ROW_PART_KEYS));
|
||||||
</SemanticTemplatePartView>
|
// Same order the rendered row composes in, so a template that ships `nowrap` counts too.
|
||||||
);
|
const { flexWrap } = mergeStyles(style, resolved.style);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SemanticTemplatePartView partKeys={ITEM_HEADER_ROW_PART_KEYS} style={composeStyles(style)}>
|
||||||
|
<ItemHeaderRowNowrapContext.Provider value={flexWrap === "nowrap"}>
|
||||||
|
{children}
|
||||||
|
</ItemHeaderRowNowrapContext.Provider>
|
||||||
|
</SemanticTemplatePartView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const SectionItemHeader = ({ children }: SectionItemHeaderProps) => {
|
const SectionItemHeader = ({ children }: SectionItemHeaderProps) => {
|
||||||
const itemNodeKey = useSemanticNodeKey();
|
const itemNodeKey = useSemanticNodeKey();
|
||||||
@@ -687,12 +708,21 @@ const SectionItemHeader = ({ children }: SectionItemHeaderProps) => {
|
|||||||
|
|
||||||
const ItemTitle = ({ children, website, field, bold = true }: ItemTitleProps) => {
|
const ItemTitle = ({ children, website, field, bold = true }: ItemTitleProps) => {
|
||||||
const inlineWebsiteUrl = getInlineItemWebsiteUrl(website);
|
const inlineWebsiteUrl = getInlineItemWebsiteUrl(website);
|
||||||
const title = bold ? <Bold semanticField={field}>{children}</Bold> : <Text semanticField={field}>{children}</Text>;
|
const style = use(ItemHeaderRowNowrapContext) ? nowrapItemTitleStyle : wrappingItemTitleStyle;
|
||||||
|
const title = bold ? (
|
||||||
|
<Bold style={style} semanticField={field}>
|
||||||
|
{children}
|
||||||
|
</Bold>
|
||||||
|
) : (
|
||||||
|
<Text style={style} semanticField={field}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
|
||||||
if (!inlineWebsiteUrl) return title;
|
if (!inlineWebsiteUrl) return title;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link semanticRole="inline-website" src={inlineWebsiteUrl}>
|
<Link style={style} semanticRole="inline-website" src={inlineWebsiteUrl}>
|
||||||
{title}
|
{title}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user