Skip to content

Commit

Permalink
Add Rich Text Component (#2144)
Browse files Browse the repository at this point in the history
  • Loading branch information
blittle committed May 28, 2024
1 parent 5d6465b commit 30d18bd
Show file tree
Hide file tree
Showing 16 changed files with 831 additions and 0 deletions.
22 changes: 22 additions & 0 deletions .changeset/long-candles-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
'@shopify/hydrogen-react': minor
---

Add a RichText component to easily render \`rich_text_field\` metafields. Thank you @bastienrobert for the original implementation. Example usage:

```tsx
import {RichText} from '@shopify/hydrogen-react';

export function MainRichText({metaFieldData}: {metaFieldData: string}) {
return (
<RichText
data={metaFieldData}
components={{
paragraph({node}) {
return <p className="customClass">{node.children}</p>;
},
}}
/>
);
}
```
127 changes: 127 additions & 0 deletions packages/hydrogen-react/src/RichText.components.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import {createElement, type ReactNode} from 'react';

export type CustomComponents = {
/** The root node of the rich text. Defaults to `<div>` */
root?: typeof Root;
/** Customize the headings. Each heading has a `level` property from 1-6. Defaults to `<h1>` to `<h6>` */
heading?: typeof Heading;
/** Customize paragraphs. Defaults to `<p>` */
paragraph?: typeof Paragraph;
/** Customize how text nodes. They can either be bold or italic. Defaults to `<em>`, `<strong>` or text. */
text?: typeof Text;
/** Customize links. Defaults to a React Router `<Link>` component in Hydrogen and a `<a>` in Hydrogen React. */
link?: typeof RichTextLink;
/** Customize lists. They can be either ordered or unordered. Defaults to `<ol>` or `<ul>` */
list?: typeof List;
/** Customize list items. Defaults to `<li>`. */
listItem?: typeof ListItem;
};

export const RichTextComponents = {
root: Root,
heading: Heading,
paragraph: Paragraph,
text: Text,
link: RichTextLink,
list: List,
'list-item': ListItem,
};

function Root({
node,
}: {
node: {
type: 'root';
children?: ReactNode[];
};
}): ReactNode {
return <div>{node.children}</div>;
}

function Heading({
node,
}: {
node: {
type: 'heading';
level: number;
children?: ReactNode[];
};
}): ReactNode {
return createElement(`h${node.level ?? '1'}`, null, node.children);
}

function Paragraph({
node,
}: {
node: {
type: 'paragraph';
children?: ReactNode[];
};
}): ReactNode {
return <p>{node.children}</p>;
}

function Text({
node,
}: {
node: {
type: 'text';
italic?: boolean;
bold?: boolean;
value?: string;
};
}): ReactNode {
if (node.bold && node.italic)
return (
<em>
<strong>{node.value}</strong>
</em>
);

if (node.bold) return <strong>{node.value}</strong>;
if (node.italic) return <em>{node.value}</em>;

return node.value;
}

function RichTextLink({
node,
}: {
node: {
type: 'link';
url: string;
title?: string;
target?: string;
children?: ReactNode[];
};
}): ReactNode {
return (
<a href={node.url} title={node.title} target={node.target}>
{node.children}
</a>
);
}

function List({
node,
}: {
node: {
type: 'list';
listType: 'unordered' | 'ordered';
children?: ReactNode[];
};
}): ReactNode {
const List = node.listType === 'unordered' ? 'ul' : 'ol';
return <List>{node.children}</List>;
}

function ListItem({
node,
}: {
node: {
type: 'list-item';
children?: ReactNode[];
};
}): ReactNode {
return <li>{node.children}</li>;
}
37 changes: 37 additions & 0 deletions packages/hydrogen-react/src/RichText.doc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs';

const data: ReferenceEntityTemplateSchema = {
name: 'RichText',
category: 'components',
isVisualComponent: false,
related: [],
description: `The \`RichText\` component renders a metafield of type \`rich_text_field\`. By default the rendered output uses semantic HTML tags. Customize how nodes are rendered with the \`components\` prop.`,
type: 'component',
defaultExample: {
description: 'I am the default example',
codeblock: {
tabs: [
{
title: 'JavaScript',
code: './RichText.example.jsx',
language: 'jsx',
},
{
title: 'TypeScript',
code: './RichText.example.tsx',
language: 'tsx',
},
],
title: 'Example code',
},
},
definitions: [
{
title: 'Props',
type: 'RichTextPropsForDocs',
description: '',
},
],
};

export default data;
14 changes: 14 additions & 0 deletions packages/hydrogen-react/src/RichText.example.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {RichText} from '@shopify/hydrogen-react';

export function MainRichText({metaFieldData}) {
return (
<RichText
data={metaFieldData}
components={{
paragraph({node}) {
return <p className="customClass">{node.children}</p>;
},
}}
/>
);
}
14 changes: 14 additions & 0 deletions packages/hydrogen-react/src/RichText.example.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {RichText} from '@shopify/hydrogen-react';

export function MainRichText({metaFieldData}: {metaFieldData: string}) {
return (
<RichText
data={metaFieldData}
components={{
paragraph({node}) {
return <p className="customClass">{node.children}</p>;
},
}}
/>
);
}
16 changes: 16 additions & 0 deletions packages/hydrogen-react/src/RichText.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react';
import type {Story} from '@ladle/react';
import {RichText} from './RichText.js';
import {RICH_TEXT_CONTENT} from './RichText.test.helpers.js';

type RichTextProps = React.ComponentProps<typeof RichText>;

const Template: Story<RichTextProps> = (props: RichTextProps) => {
return <RichText {...props}>Add to cart</RichText>;
};

export const Default = Template.bind({});
Default.args = {
as: 'div',
data: JSON.stringify(RICH_TEXT_CONTENT),
};
151 changes: 151 additions & 0 deletions packages/hydrogen-react/src/RichText.test.helpers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import {type RichTextASTNode} from './RichText.types.js';

export const RICH_TEXT_HEADING_1: RichTextASTNode = {
type: 'heading',
children: [
{
type: 'text',
value: 'Heading 1',
},
],
level: 1,
};

export const RICH_TEXT_HEADING_2: RichTextASTNode = {
type: 'heading',
level: 2,
children: [
{
type: 'text',
value: 'Heading 2',
},
],
};

export const RICH_TEXT_PARAGRAPH: RichTextASTNode = {
type: 'paragraph',
children: [
{
type: 'text',
value: 'Paragraph',
},
],
};

export const RICH_TEXT_COMPLEX_PARAGRAPH: RichTextASTNode = {
type: 'paragraph',
children: [
{
type: 'text',
value: 'This',
italic: true,
},
{
type: 'text',
value: ' is a ',
},
{
type: 'text',
value: 'text',
bold: true,
},
{
type: 'text',
value: ' and a ',
},
{
url: '/products/foo',
title: 'title',
target: '_blank',
type: 'link',
children: [
{
type: 'text',
value: 'link',
},
],
},
{
type: 'text',
value: ' and an ',
},
{
url: 'https://shopify.com',
title: 'Title',
target: '_blank',
type: 'link',
children: [
{
type: 'text',
value: 'external link',
},
],
},
{
type: 'text',
value: '',
},
],
};

export const RICH_TEXT_ORDERED_LIST: RichTextASTNode = {
listType: 'ordered',
type: 'list',
children: [
{
type: 'list-item',
children: [
{
type: 'text',
value: 'One',
},
],
},
{
type: 'list-item',
children: [
{
type: 'text',
value: 'Two',
},
],
},
],
};

export const RICH_TEXT_UNORDERED_LIST: RichTextASTNode = {
listType: 'unordered',
type: 'list',
children: [
{
type: 'list-item',
children: [
{
type: 'text',
value: 'One',
},
],
},
{
type: 'list-item',
children: [
{
type: 'text',
value: 'Two',
},
],
},
],
};

export const RICH_TEXT_CONTENT: RichTextASTNode = {
type: 'root',
children: [
RICH_TEXT_HEADING_1,
RICH_TEXT_HEADING_2,
RICH_TEXT_PARAGRAPH,
RICH_TEXT_COMPLEX_PARAGRAPH,
RICH_TEXT_ORDERED_LIST,
RICH_TEXT_UNORDERED_LIST,
],
};
Loading

0 comments on commit 30d18bd

Please sign in to comment.