Skip to content

Commit

Permalink
chore(clerk-js,types,localization): Introduce LinkRenderer
Browse files Browse the repository at this point in the history
  • Loading branch information
octoper committed Oct 18, 2024
1 parent 4f7eab9 commit ad85523
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 42 deletions.
65 changes: 31 additions & 34 deletions packages/clerk-js/src/ui/elements/LegalConsentCheckbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,51 @@ import {
descriptors,
Flex,
FormLabel,
Link,
localizationKeys,
Text,
useAppearance,
useLocalizations,
} from '../customizables';
import type { PropsOfComponent } from '../styledSystem';
import { Field } from './FieldControl';
import { LinkRenderer } from './LinkRenderer';

const LegalCheckboxLabel = (props: { termsUrl?: string; privacyPolicyUrl?: string }) => {
const { termsUrl, privacyPolicyUrl } = props;
const { t } = useLocalizations();
let localizationKey: LocalizationKey | undefined;

if (termsUrl && privacyPolicyUrl) {
localizationKey = localizationKeys(
'signUp.__experimental_legalConsent.checkbox.label__termsOfServiceAndPrivacyPolicy',
{
termsOfServiceLink: props.termsUrl,
privacyPolicyLink: props.privacyPolicyUrl,
},
);
} else if (termsUrl) {
localizationKey = localizationKeys('signUp.__experimental_legalConsent.checkbox.label__onlyTermsOfService', {
termsOfServiceLink: props.termsUrl,
});
} else if (privacyPolicyUrl) {
localizationKey = localizationKeys('signUp.__experimental_legalConsent.checkbox.label__onlyPrivacyPolicy', {
privacyPolicyLink: props.privacyPolicyUrl,
});
}

return (
<Text
variant='body'
as='span'
>
{t(localizationKeys('signUp.__experimental_legalConsent.checkbox.label__prefixText'))}
{props.termsUrl && (
<>
{' '}
<Link
localizationKey={localizationKeys('signUp.__experimental_legalConsent.checkbox.label__termsOfServiceText')}
href={props.termsUrl}
sx={{
textDecoration: 'underline',
}}
isExternal
/>
</>
)}

{props.termsUrl && props.privacyPolicyUrl && (
<> {t(localizationKeys('signUp.__experimental_legalConsent.checkbox.label__conjunctionText'))} </>
)}

{props.privacyPolicyUrl && (
<>
{' '}
<Link
localizationKey={localizationKeys('signUp.__experimental_legalConsent.checkbox.label__privacyPolicyText')}
href={props.termsUrl}
sx={{
textDecoration: 'underline',
display: 'inline-block',
}}
isExternal
/>
</>
)}
<LinkRenderer
text={t(localizationKey)}
isExternal
sx={t => ({
textDecoration: 'underline',
textUnderlineOffset: t.space.$1,
})}
/>
</Text>
);
};
Expand Down Expand Up @@ -82,6 +78,7 @@ export const LegalCheckbox = (
htmlFor={props.itemID}
sx={t => ({
paddingLeft: t.space.$1x5,
textAlign: 'left',
})}
>
<LegalCheckboxLabel
Expand Down
44 changes: 44 additions & 0 deletions packages/clerk-js/src/ui/elements/LinkRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { memo, useMemo } from 'react';

import { Link } from '../customizables';
import type { PropsOfComponent } from '../styledSystem';

interface LinkRendererProps extends Omit<PropsOfComponent<typeof Link>, 'href' | 'children'> {
text: string;
}

const LINK_REGEX = /\[([^\]]+)\]\(([^)]+)\)/g; // parses [text](url)

export const LinkRenderer: React.FC<LinkRendererProps> = memo(({ text, ...linkProps }) => {
const memoizedLinkProps = useMemo(() => linkProps, [linkProps]);

const renderedContent = useMemo(() => {
const parts: (string | JSX.Element)[] = [];
let lastIndex = 0;

text.replace(LINK_REGEX, (match, linkText, url, offset) => {
if (offset > lastIndex) {
parts.push(text.slice(lastIndex, offset));
}
parts.push(
<Link
key={offset}
href={url}
{...memoizedLinkProps}
>
{linkText}
</Link>,
);
lastIndex = offset + match.length;
return match;
});

if (lastIndex < text.length) {
parts.push(text.slice(lastIndex));
}

return parts;
}, [text, memoizedLinkProps]);

return renderedContent;
});
44 changes: 44 additions & 0 deletions packages/clerk-js/src/ui/elements/__tests__/LinkRenderer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, it } from '@jest/globals';
import { render } from '@testing-library/react';
import React from 'react';

import { bindCreateFixtures } from '../../utils/test/createFixtures';
import { LinkRenderer } from '../LinkRenderer';

const { createFixtures } = bindCreateFixtures('UserProfile');

describe('LinkRenderer', () => {
it('renders a simple link', async () => {
const { wrapper } = await createFixtures();

const screen = render(<LinkRenderer text='I agree to the [Terms of Service](https://example.com/terms)' />, {
wrapper,
});

expect(screen.queryByRole('link', { name: 'Terms of Service' })).toBeInTheDocument();
});

it('renders multiple links', async () => {
const { wrapper } = await createFixtures();

const screen = render(
<LinkRenderer text='I agree to the [Terms of Service](https://example.com/terms) and [Privacy Policy](https://example.com/privacy)' />,
{ wrapper },
);

expect(screen.queryByRole('link', { name: 'Terms of Service' })).toBeInTheDocument();
expect(screen.queryByRole('link', { name: 'Privacy Policy' })).toBeInTheDocument();
});

it('does not render links with broken format', async () => {
const { wrapper } = await createFixtures();

const screen = render(
<LinkRenderer text='I agree to the [Terms of Service]https://example.com/terms) and [Privacy Policy](https://example.com/privacy)' />,
{ wrapper },
);

screen.findByText('[Terms of Service]https://example.com/terms)');
expect(screen.queryByRole('link', { name: 'Privacy Policy' })).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,14 @@ const numeric = (val: Date | number | string, locale?: string) => {
}
};

const link = (val: string, label?: string) => {
return `[${label}](${val})`;
};

export const MODIFIERS = {
titleize,
timeString,
weekday,
numeric,
link,
} as const;
8 changes: 4 additions & 4 deletions packages/localizations/src/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,10 +521,10 @@ export const enUS: LocalizationResource = {
title: 'Legal consent',
},
checkbox: {
label__prefixText: 'I agree to the',
label__privacyPolicyText: 'Privacy Policy',
label__termsOfServiceText: 'Terms of Service',
label__conjunctionText: 'and',
label__termsOfServiceAndPrivacyPolicy:
'I agree to the {{ termsOfServiceLink || link("Terms of Service") }} and {{ privacyPolicyLink || link("Privacy Policy") }}',
label__onlyTermsOfService: 'I agree to the {{ termsOfServiceLink || link("Terms of Service") }}',
label__onlyPrivacyPolicy: 'I agree to the {{ privacyPolicyLink || link("Privacy Policy") }}',
},
},
},
Expand Down
7 changes: 3 additions & 4 deletions packages/types/src/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,9 @@ type _LocalizationResource = {
subtitle: LocalizationValue;
};
checkbox: {
label__prefixText: LocalizationValue;
label__termsOfServiceText: LocalizationValue;
label__privacyPolicyText: LocalizationValue;
label__conjunctionText: LocalizationValue;
label__termsOfServiceAndPrivacyPolicy: LocalizationValue;
label__onlyPrivacyPolicy: LocalizationValue;
label__onlyTermsOfService: LocalizationValue;
};
};
};
Expand Down

0 comments on commit ad85523

Please sign in to comment.