Skip to content

Commit

Permalink
feat: support links in checkbox/boolean nodes (#251)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonas-jonas authored Oct 29, 2024
1 parent 9190b15 commit cc92b9c
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 5 deletions.
9 changes: 6 additions & 3 deletions packages/elements-react/src/tests/jest/test-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import { OryProvider, OryProviderProps } from "../../context"
import { OryComponentProvider } from "../../context/component"
import { OryDefaultComponents } from "../../theme/default"
import { OryClientConfiguration } from "../../util"
import { IntlProvider } from "../../context/intl-context"

const AllProviders = ({ children }: PropsWithChildren) => (
<OryComponentProvider components={OryDefaultComponents}>
{children}
</OryComponentProvider>
<IntlProvider locale="en">
<OryComponentProvider components={OryDefaultComponents}>
{children}
</OryComponentProvider>
</IntlProvider>
)

const customRender = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
"use client"
import { getNodeLabel } from "@ory/client-fetch"
import {
OryNodeInputProps,
messageTestId,
OryNodeInputProps,
uiTextToFormattedMessage,
} from "@ory/elements-react"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { cn } from "../../utils/cn"
import { CheckboxLabel } from "../ui/checkbox-label"

function CheckboxSVG() {
return (
Expand Down Expand Up @@ -82,7 +83,7 @@ export const DefaultCheckbox = ({
</div>
<div className="text-sm items-center">
<label className="text-sm font-normal leading-normal text-forms-fg-default">
{label && uiTextToFormattedMessage(label, intl)}
<CheckboxLabel label={label} />
</label>
{node.messages.map((message) => (
<span
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`computeLabelElements handles a label with no text but a link 1`] = `
<div>
<a
class="text-links-link-default hover:text-links-link-hover hover:underline"
href="https://example.com"
rel="noopener noreferrer"
target="_blank"
>
Click here
</a>
</div>
`;

exports[`computeLabelElements renders a text with a single markdown link correctly 1`] = `
<div>
This is a
<a
class="text-links-link-default hover:text-links-link-hover hover:underline"
href="https://example.com"
rel="noopener noreferrer"
target="_blank"
>
link
</a>
</div>
`;

exports[`computeLabelElements renders a text with link and extra text around it correctly 1`] = `
<div>
Click
<a
class="text-links-link-default hover:text-links-link-hover hover:underline"
href="https://example.com"
rel="noopener noreferrer"
target="_blank"
>
here
</a>
to visit, or go elsewhere.
</div>
`;

exports[`computeLabelElements renders a text with multiple markdown links correctly 1`] = `
<div>
This
<a
class="text-links-link-default hover:text-links-link-hover hover:underline"
href="https://first.com"
rel="noopener noreferrer"
target="_blank"
>
first link
</a>
and this
<a
class="text-links-link-default hover:text-links-link-hover hover:underline"
href="https://second.com"
rel="noopener noreferrer"
target="_blank"
>
second link
</a>
</div>
`;

exports[`computeLabelElements renders null if label is undefined 1`] = `<div />`;

exports[`computeLabelElements renders plain text without links correctly 1`] = `
<div>
This is just plain text
</div>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { render } from "../../../../tests/jest/test-utils"
import { CheckboxLabel } from "./checkbox-label"

describe("computeLabelElements", () => {
test("renders plain text without links correctly", () => {
const labelText = "This is just plain text"

const { container } = render(
<CheckboxLabel label={{ text: labelText, id: 0, type: "info" }} />,
)
expect(container).toMatchSnapshot()
})

test("renders a text with a single markdown link correctly", () => {
const labelText = "This is a [link](https://example.com)"

const { container } = render(
<CheckboxLabel label={{ text: labelText, id: 0, type: "info" }} />,
)
expect(container).toMatchSnapshot()
})

test("renders a text with multiple markdown links correctly", () => {
const labelText =
"This [first link](https://first.com) and this [second link](https://second.com)"

const { container } = render(
<CheckboxLabel label={{ text: labelText, id: 0, type: "info" }} />,
)
expect(container).toMatchSnapshot()
})

test("renders a text with link and extra text around it correctly", () => {
const labelText =
"Click [here](https://example.com) to visit, or go elsewhere."

const { container } = render(
<CheckboxLabel label={{ text: labelText, id: 0, type: "info" }} />,
)
expect(container).toMatchSnapshot()
})

test("handles a label with no text but a link", () => {
const labelText = "[Click here](https://example.com)"

const { container } = render(
<CheckboxLabel label={{ text: labelText, id: 0, type: "info" }} />,
)
expect(container).toMatchSnapshot()
})

test("renders null if label is undefined", () => {
const { container } = render(<CheckboxLabel label={undefined} />)
expect(container).toMatchSnapshot()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { UiText } from "@ory/client-fetch"
import { useIntl } from "react-intl"
import { uiTextToFormattedMessage } from "../../../../util"

type CheckboxLabelProps = {
label?: UiText
}

const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g

export function computeLabelElements(labelText: string) {
const elements = []
let lastIndex = 0

// Use matchAll to find all markdown links
for (const match of labelText.matchAll(linkRegex)) {
const linkText = match[1]
const url = match[2]
const matchStart = match.index
if (typeof matchStart === "undefined") {
// Some types seem to be wrong somewhere, eslint complains that matchStart can be undefined, but it can't?
// So we just skip this match, if it is undefined
continue
}

// Push the text before the match
if (matchStart > lastIndex) {
elements.push(labelText.slice(lastIndex, matchStart))
}

// Push the <a> tag for the markdown link
elements.push(
<a
key={matchStart}
href={url}
target="_blank"
rel="noopener noreferrer"
className="text-links-link-default hover:text-links-link-hover hover:underline"
>
{linkText}
</a>,
)

// Update lastIndex to the end of the current match
lastIndex = matchStart + match[0].length
}

// Push any remaining text after the last match
if (lastIndex < labelText.length) {
elements.push(labelText.slice(lastIndex))
}
return elements
}

export function CheckboxLabel({ label }: CheckboxLabelProps) {
const intl = useIntl()
if (!label) {
return null
}

const labelText = uiTextToFormattedMessage(label, intl)

return <>{computeLabelElements(labelText)}</>
}

0 comments on commit cc92b9c

Please sign in to comment.