Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔨 use markdown for rendering grouped text wraps / TAS-782 #4402

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
Expand Up @@ -166,3 +166,87 @@ describe("MarkdownTextWrap", () => {
})
})
})

describe("fromFragments", () => {
const fontSize = 14

it("should place fragments in-line by default", () => {
const textWrap = MarkdownTextWrap.fromFragments({
main: { text: "Lower middle-income countries" },
secondary: { text: "30 million" },
textWrapProps: {
maxWidth: 500,
fontSize,
},
})
expect(textWrap.svgLines.length).toEqual(1)
expect(textWrap.htmlLines.length).toEqual(1)
})

it("should place the secondary text in a new line if requested", () => {
const textWrap = MarkdownTextWrap.fromFragments({
main: { text: "Lower middle-income countries" },
secondary: { text: "30 million" },
newLine: "always",
textWrapProps: {
maxWidth: 1000,
fontSize,
},
})
expect(textWrap.svgLines.length).toEqual(2)
expect(textWrap.htmlLines.length).toEqual(2)
})

it("should place the secondary text in a new line if line breaks should be avoided", () => {
const textWrap = MarkdownTextWrap.fromFragments({
main: { text: "Lower middle-income countries" },
secondary: { text: "30 million" },
newLine: "avoid-wrap",
textWrapProps: {
maxWidth: 250,
fontSize,
},
})
expect(textWrap.svgLines.length).toEqual(2)
expect(textWrap.htmlLines.length).toEqual(2)
})

it("should place the secondary text in the same line if possible", () => {
const textWrap = MarkdownTextWrap.fromFragments({
main: { text: "Lower middle-income countries" },
secondary: { text: "30 million" },
newLine: "avoid-wrap",
textWrapProps: {
maxWidth: 1000,
fontSize,
},
})
expect(textWrap.svgLines.length).toEqual(1)
expect(textWrap.htmlLines.length).toEqual(1)
})

it("should use all available space when one fragment exceeds the given max width", () => {
const textWrap = MarkdownTextWrap.fromFragments({
main: { text: "Long-word-that-can't-be-broken-up more words" },
secondary: { text: "30 million" },
textWrapProps: {
maxWidth: 150,
fontSize,
},
})
expect(textWrap.width).toBeGreaterThan(150)
})

it("should place very long words in a separate line", () => {
const textWrap = MarkdownTextWrap.fromFragments({
main: { text: "30 million" },
secondary: { text: "Long-word-that-can't-be-broken-up" },
textWrapProps: {
maxWidth: 150,
fontSize,
},
})
expect(textWrap.svgLines.length).toEqual(2)
expect(textWrap.htmlLines.length).toEqual(2)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -507,18 +507,83 @@ export const sumTextWrapHeights = (
sum(elements.map((element) => element.height)) +
(elements.length - 1) * spacer

type MarkdownTextWrapProps = {
text: string
fontSize: number
type MarkdownTextWrapOptions = {
maxWidth?: number
fontFamily?: FontFamily
fontSize: number
fontWeight?: number
lineHeight?: number
maxWidth?: number
style?: CSSProperties
detailsOrderedByReference?: string[]
}

type MarkdownTextWrapProps = { text: string } & MarkdownTextWrapOptions

type TextFragment = { text: string; bold?: boolean }

export class MarkdownTextWrap extends React.Component<MarkdownTextWrapProps> {
static fromFragments({
main,
secondary,
newLine = "continue-line",
textWrapProps,
}: {
main: TextFragment
secondary: TextFragment
newLine?: "continue-line" | "always" | "avoid-wrap"
textWrapProps: Omit<MarkdownTextWrapOptions, "fontWeight">
}) {
const mainMarkdownText = maybeBoldMarkdownText(main)
const secondaryMarkdownText = maybeBoldMarkdownText(secondary)

const combinedTextContinued = [
mainMarkdownText,
secondaryMarkdownText,
].join(" ")
const combinedTextNewLine = [
mainMarkdownText,
secondaryMarkdownText,
].join("\n")

if (newLine === "always") {
return new MarkdownTextWrap({
text: combinedTextNewLine,
...textWrapProps,
})
}

if (newLine === "continue-line") {
return new MarkdownTextWrap({
text: combinedTextContinued,
...textWrapProps,
})
}

// if newLine is set to 'avoid-wrap', we first try to fit the secondary text
// on the same line as the main text. If it doesn't fit, we place it on a new line.

const mainTextWrap = new MarkdownTextWrap({ ...main, ...textWrapProps })
const secondaryTextWrap = new MarkdownTextWrap({
text: secondaryMarkdownText,
...textWrapProps,
maxWidth: mainTextWrap.maxWidth - mainTextWrap.lastLineWidth,
})

const secondaryTextFitsOnSameLine =
secondaryTextWrap.svgLines.length === 1
if (secondaryTextFitsOnSameLine) {
return new MarkdownTextWrap({
text: combinedTextContinued,
...textWrapProps,
})
} else {
return new MarkdownTextWrap({
text: combinedTextNewLine,
...textWrapProps,
})
}
}

@computed get maxWidth(): number {
return this.props.maxWidth ?? Infinity
}
Expand Down Expand Up @@ -602,10 +667,18 @@ export class MarkdownTextWrap extends React.Component<MarkdownTextWrapProps> {
return max(lineLengths) ?? 0
}

@computed get singleLineHeight(): number {
return this.fontSize * this.lineHeight
}

@computed get lastLineWidth(): number {
return sumBy(last(this.htmlLines), (token) => token.width) ?? 0
}

@computed get height(): number {
const { htmlLines, lineHeight, fontSize } = this
const { htmlLines } = this
if (htmlLines.length === 0) return 0
return htmlLines.length * lineHeight * fontSize
return htmlLines.length * this.singleLineHeight
}

@computed get style(): any {
Expand Down Expand Up @@ -648,13 +721,13 @@ export class MarkdownTextWrap extends React.Component<MarkdownTextWrapProps> {
detailsMarker?: DetailsMarker
id?: string
} = {}
): React.ReactElement | null {
): React.ReactElement {
const { fontSize, lineHeight } = this
const lines =
detailsMarker === "superscript"
? this.svgLinesWithDodReferenceNumbers
: this.svgLines
if (lines.length === 0) return null
if (lines.length === 0) return <></>

// Magic number set through experimentation.
// The HTML and SVG renderers need to position lines identically.
Expand Down Expand Up @@ -1092,3 +1165,13 @@ function appendReferenceNumbers(

return appendedTokens
}

function maybeBoldMarkdownText({
text,
bold,
}: {
text: string
bold?: boolean
}): string {
return bold ? `**${text}**` : text
}
57 changes: 0 additions & 57 deletions packages/@ourworldindata/components/src/TextWrap/TextWrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,60 +145,3 @@ describe("lines()", () => {
])
})
})

describe("firstLineOffset", () => {
it("should offset the first line if requested", () => {
const text = "an example line"
const props = { text, maxWidth: 100, fontSize: FONT_SIZE }

const wrapWithoutOffset = new TextWrap(props)
const wrapWithOffset = new TextWrap({
...props,
firstLineOffset: 50,
})

expect(wrapWithoutOffset.lines.map((l) => l.text)).toEqual([
"an example",
"line",
])
expect(wrapWithOffset.lines.map((l) => l.text)).toEqual([
"an",
"example line",
])
})

it("should break into a new line even if the first line would end up being empty", () => {
const text = "a-very-long-word"
const props = { text, maxWidth: 100, fontSize: FONT_SIZE }

const wrapWithoutOffset = new TextWrap(props)
const wrapWithOffset = new TextWrap({
...props,
firstLineOffset: 50,
})

expect(wrapWithoutOffset.lines.map((l) => l.text)).toEqual([
"a-very-long-word",
])
expect(wrapWithOffset.lines.map((l) => l.text)).toEqual([
"",
"a-very-long-word",
])
})

it("should break into a new line if firstLineOffset > maxWidth", () => {
const text = "an example line"
const wrap = new TextWrap({
text,
maxWidth: 100,
fontSize: FONT_SIZE,
firstLineOffset: 150,
})

expect(wrap.lines.map((l) => l.text)).toEqual([
"",
"an example",
"line",
])
})
})
Loading
Loading