Skip to content

Commit

Permalink
fix(tailwind): className manipulation for component props (#1556)
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielmfern committed Aug 22, 2024
1 parent 6d27a20 commit 5018ce8
Show file tree
Hide file tree
Showing 8 changed files with 55 additions and 29 deletions.
6 changes: 6 additions & 0 deletions .changeset/many-donuts-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@react-email/tailwind": minor
---

- Add support for proper `className` manipulation
- Make inline styles override Tailwind styles.
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,30 @@ exports[`email export 1`] = `
</div>
<body style="background-color:rgb(255,255,255);margin-top:auto;margin-bottom:auto;margin-left:auto;margin-right:auto;font-family:ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto, &quot;Helvetica Neue&quot;, Arial, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;;padding-left:0.5rem;padding-right:0.5rem">
<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:465px;border-width:1px;border-style:solid;border-color:rgb(234,234,234);border-radius:0.25rem;margin-top:40px;margin-bottom:40px;margin-left:auto;margin-right:auto;padding:20px">
<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="border-width:1px;border-style:solid;border-color:rgb(234,234,234);border-radius:0.25rem;margin-top:40px;margin-bottom:40px;margin-left:auto;margin-right:auto;padding:20px;max-width:465px">
<tbody>
<tr style="width:100%">
<td>
<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-top:32px">
<tbody>
<tr>
<td><img alt="Vercel" height="37" src="/static/vercel-logo.png" style="display:block;outline:none;border:none;text-decoration:none;margin-top:0px;margin-bottom:0px;margin-left:auto;margin-right:auto" width="40" /></td>
<td><img alt="Vercel" height="37" src="/static/vercel-logo.png" style="margin-top:0px;margin-bottom:0px;margin-left:auto;margin-right:auto;display:block;outline:none;border:none;text-decoration:none" width="40" /></td>
</tr>
</tbody>
</table>
<h1 style="color:rgb(0,0,0);font-size:24px;font-weight:400;text-align:center;padding:0px;margin-top:30px;margin-bottom:30px;margin-left:0px;margin-right:0px">Join <strong></strong> on <strong>Vercel</strong></h1>
<p style="font-size:14px;line-height:24px;margin:16px 0;color:rgb(0,0,0)">Hello <!-- -->,</p>
<p style="font-size:14px;line-height:24px;margin:16px 0;color:rgb(0,0,0)"><strong></strong> (<a href="mailto:undefined" style="color:rgb(37,99,235);text-decoration:none;text-decoration-line:none" target="_blank"></a>) has invited you to the <strong></strong> team on<!-- --> <strong>Vercel</strong>.</p>
<p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin:16px 0">Hello <!-- -->,</p>
<p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin:16px 0"><strong></strong> (<a href="mailto:undefined" style="color:rgb(37,99,235);text-decoration-line:none;text-decoration:none" target="_blank"></a>) has invited you to the <strong></strong> team on<!-- --> <strong>Vercel</strong>.</p>
<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation">
<tbody>
<tr>
<td>
<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation">
<tbody style="width:100%">
<tr style="width:100%">
<td align="right" data-id="__react-email-column"><img height="64" style="display:block;outline:none;border:none;text-decoration:none;border-radius:9999px" width="64" /></td>
<td align="right" data-id="__react-email-column"><img height="64" style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64" /></td>
<td align="center" data-id="__react-email-column"><img alt="invited you to" height="9" src="/static/vercel-arrow.png" style="display:block;outline:none;border:none;text-decoration:none" width="12" /></td>
<td align="left" data-id="__react-email-column"><img height="64" style="display:block;outline:none;border:none;text-decoration:none;border-radius:9999px" width="64" /></td>
<td align="left" data-id="__react-email-column"><img height="64" style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64" /></td>
</tr>
</tbody>
</table>
Expand All @@ -48,13 +48,13 @@ exports[`email export 1`] = `
<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:center;margin-top:32px;margin-bottom:32px">
<tbody>
<tr>
<td><a style="line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;background-color:rgb(0,0,0);border-radius:0.25rem;color:rgb(255,255,255);font-size:12px;font-weight:600;text-decoration-line:none;text-align:center;padding-left:1.25rem;padding-right:1.25rem;padding-top:0.75rem;padding-bottom:0.75rem;padding:12px 20px 12px 20px" target="_blank"><span><!--[if mso]><i style="mso-font-width:500%;mso-text-raise:18" hidden>&#8202;&#8202;</i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px">Join the team</span><span><!--[if mso]><i style="mso-font-width:500%" hidden>&#8202;&#8202;&#8203;</i><![endif]--></span></a></td>
<td><a style="background-color:rgb(0,0,0);border-radius:0.25rem;color:rgb(255,255,255);font-size:12px;font-weight:600;text-decoration-line:none;text-align:center;padding-left:1.25rem;padding-right:1.25rem;padding-top:0.75rem;padding-bottom:0.75rem;line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;padding:12px 20px 12px 20px" target="_blank"><span><!--[if mso]><i style="mso-font-width:500%;mso-text-raise:18" hidden>&#8202;&#8202;</i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px">Join the team</span><span><!--[if mso]><i style="mso-font-width:500%" hidden>&#8202;&#8202;&#8203;</i><![endif]--></span></a></td>
</tr>
</tbody>
</table>
<p style="font-size:14px;line-height:24px;margin:16px 0;color:rgb(0,0,0)">or copy and paste this URL into your browser:<!-- --> <a style="color:rgb(37,99,235);text-decoration:none;text-decoration-line:none" target="_blank"></a></p>
<hr style="width:100%;border:none;border-top:1px solid #eaeaea;border-width:1px;border-style:solid;border-color:rgb(234,234,234);margin-top:26px;margin-bottom:26px;margin-left:0px;margin-right:0px" />
<p style="font-size:12px;line-height:24px;margin:16px 0;color:rgb(102,102,102)">This invitation was intended for<!-- --> <span style="color:rgb(0,0,0)"></span>. This invite was sent from <span style="color:rgb(0,0,0)"></span> <!-- -->located in<!-- --> <span style="color:rgb(0,0,0)"></span>. If you were not expecting this invitation, you can ignore this email. If you are concerned about your account&#x27;s safety, please reply to this email to get in touch with us.</p>
<p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin:16px 0">or copy and paste this URL into your browser:<!-- --> <a style="color:rgb(37,99,235);text-decoration-line:none;text-decoration:none" target="_blank"></a></p>
<hr style="border-width:1px;border-style:solid;border-color:rgb(234,234,234);margin-top:26px;margin-bottom:26px;margin-left:0px;margin-right:0px;width:100%;border:none;border-top:1px solid #eaeaea" />
<p style="color:rgb(102,102,102);font-size:12px;line-height:24px;margin:16px 0">This invitation was intended for<!-- --> <span style="color:rgb(0,0,0)"></span>. This invite was sent from <span style="color:rgb(0,0,0)"></span> <!-- -->located in<!-- --> <span style="color:rgb(0,0,0)"></span>. If you were not expecting this invitation, you can ignore this email. If you are concerned about your account&#x27;s safety, please reply to this email to get in touch with us.</p>
</td>
</tr>
</tbody>
Expand Down

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ If you do already have a <head> element at some depth, please file a bug https:/
exports[`Responsive styles > should work with relatively complex media query utilities 1`] = `"<head><meta content=\\"text/html; charset=UTF-8\\" http-equiv=\\"Content-Type\\"/><meta name=\\"x-apple-disable-message-reformatting\\"/><style>@media not all and(min-width:640px){.max-sm_text-red-600{color:rgb(220,38,38)!important}}</style></head><p class=\\"max-sm_text-red-600\\" style=\\"color:rgb(29,78,216)\\">I am some text</p>"`;
exports[`Tailwind component > <Button className="px-3 py-2 mt-8 text-sm text-gray-200 bg-blue-600 rounded-md"> 1`] = `"<a style=\\"line-height:1.25rem;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;padding-left:0.75rem;padding-right:0.75rem;padding-top:0.5rem;padding-bottom:0.5rem;margin-top:2rem;font-size:0.875rem;color:rgb(229,231,235);background-color:rgb(37,99,235);border-radius:0.375rem;padding:8px 12px 8px 12px\\" target=\\"_blank\\"><span><!--[if mso]><i style=\\"mso-font-width:300%;mso-text-raise:12\\" hidden>&#8202;&#8202;</i><![endif]--></span><span style=\\"max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:6px\\">Testing button</span><span><!--[if mso]><i style=\\"mso-font-width:300%\\" hidden>&#8202;&#8202;&#8203;</i><![endif]--></span></a>Testing"`;
exports[`Tailwind component > <Button className="px-3 py-2 mt-8 text-sm text-gray-200 bg-blue-600 rounded-md"> 1`] = `"<a style=\\"padding-left:0.75rem;padding-right:0.75rem;padding-top:0.5rem;padding-bottom:0.5rem;margin-top:2rem;font-size:0.875rem;line-height:1.25rem;color:rgb(229,231,235);background-color:rgb(37,99,235);border-radius:0.375rem;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;padding:8px 12px 8px 12px\\" target=\\"_blank\\"><span><!--[if mso]><i style=\\"mso-font-width:300%;mso-text-raise:12\\" hidden>&#8202;&#8202;</i><![endif]--></span><span style=\\"max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:6px\\">Testing button</span><span><!--[if mso]><i style=\\"mso-font-width:300%\\" hidden>&#8202;&#8202;&#8203;</i><![endif]--></span></a>Testing"`;
exports[`Tailwind component > should allow for complex children manipulation 1`] = `"<table align=\\"center\\" width=\\"100%\\" border=\\"0\\" cellPadding=\\"0\\" cellSpacing=\\"0\\" role=\\"presentation\\" style=\\"text-align:center;font-size:0;padding:0px 0px 0px 0px\\"><tbody><tr><td><table align=\\"center\\" width=\\"100%\\" border=\\"0\\" cellPadding=\\"0\\" cellSpacing=\\"0\\" role=\\"presentation\\" style=\\"max-width:300px;display:inline-block;vertical-align:top;font-size:16px;box-sizing:border-box\\"><tbody><tr><td>This is the first column</td></tr></tbody></table><table align=\\"center\\" width=\\"100%\\" border=\\"0\\" cellPadding=\\"0\\" cellSpacing=\\"0\\" role=\\"presentation\\" style=\\"max-width:300px;display:inline-block;vertical-align:top;font-size:16px;box-sizing:border-box\\"><tbody><tr><td>This is the second column</td></tr></tbody></table></td></tr></tbody></table>"`;
exports[`Tailwind component > should not override inline styles with Tailwind styles 1`] = `"<div style=\\"background-color:red;font-size:12px\\"></div>"`;
exports[`Tailwind component > should work with Heading component 1`] = `"Hello<h1>My testing heading</h1>friends"`;
exports[`Tailwind component > should work with class manipulation done on components 1`] = `"<div style=\\"color:rgb(96,165,250);background-color:rgb(239,68,68)\\"></div>"`;
20 changes: 16 additions & 4 deletions packages/tailwind/src/tailwind.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ describe("Tailwind component", () => {
expect(actualOutput).toMatchSnapshot();
});

it("should work with class manipulation done on components", () => {
const MyComponnt = (props: { className?: string }) => {
return <div className={`${props.className} bg-red-500`} />;
};

expect(
render(
<Tailwind>
<MyComponnt className="text-blue-400" />
</Tailwind>,
),
).toMatchSnapshot();
});

describe("Inline styles", () => {
it("should render children with inline Tailwind styles", () => {
const actualOutput = render(
Expand Down Expand Up @@ -178,7 +192,7 @@ describe("Tailwind component", () => {
);
});

it("should override inline styles with Tailwind styles", () => {
it("should not override inline styles with Tailwind styles", () => {
const actualOutput = render(
<Tailwind>
<div
Expand All @@ -188,9 +202,7 @@ describe("Tailwind component", () => {
</Tailwind>,
);

expect(actualOutput).toMatchInlineSnapshot(
'"<div style=\\"background-color:rgb(0,0,0);font-size:16px\\"></div>"',
);
expect(actualOutput).toMatchSnapshot();
});

it("should override component styles with Tailwind styles", () => {
Expand Down
24 changes: 13 additions & 11 deletions packages/tailwind/src/tailwind.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,25 +101,27 @@ export const Tailwind: React.FC<TailwindProps> = ({ children, config }) => {
if (element.props.className) {
const { styles, residualClassName } = inline(element.props.className);
propsToOverwrite.style = {
...element.props.style,
...styles,
...element.props.style,
};
if (residualClassName.trim().length > 0) {
propsToOverwrite.className = residualClassName;
/*
if (!isComponent(element)) {
if (residualClassName.trim().length > 0) {
propsToOverwrite.className = residualClassName;
/*
We sanitize only the class names of Tailwind classes that we are not going to inline
to avoid unpredictable behavior on the user's code. If we did sanitize all class names
a user-defined class could end up also being sanitized which would lead to unexpected
behavior and bugs that are hard to track.
*/
for (const singleClass of nonInlinableClasses) {
propsToOverwrite.className = propsToOverwrite.className.replace(
singleClass,
sanitizeClassName(singleClass),
);
for (const singleClass of nonInlinableClasses) {
propsToOverwrite.className = propsToOverwrite.className.replace(
singleClass,
sanitizeClassName(singleClass),
);
}
} else {
propsToOverwrite.className = undefined;
}
} else {
propsToOverwrite.className = undefined;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`quick safe render to string 1`] = `"<div className=\\"bg-red-500 text-gray-200\\"><h1><div className=\\"user-name flex text-2xl\\"></div></h1><span className=\\"dark:bg-green-500 hover:bg-green-800 transition-colors\\"></span></div><span className=\\"dark:bg-green-500 hover:bg-green-800 transition-colors\\"></span>"`;
exports[`quick safe render to string 1`] = `"<div className=\\"bg-red-500 text-gray-200\\"><h1><Component><div className=\\"user-name flex text-2xl\\"></div></Component></h1><span className=\\"dark:bg-green-500 hover:bg-green-800 transition-colors\\"></span></div><span className=\\"dark:bg-green-500 hover:bg-green-800 transition-colors\\"></span>"`;
4 changes: 3 additions & 1 deletion packages/tailwind/src/utils/quick-safe-render-to-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ export const quickSafeRenderToString = (element: React.ReactNode): string => {
: (type as React.FC<Props>);
// If the element is a component (function component), render it
const componentRenderingResults = functionComponent(props);
return quickSafeRenderToString(componentRenderingResults);
return `<${functionComponent.name}>${quickSafeRenderToString(
componentRenderingResults,
)}</${functionComponent.name}>`;
}

// Regular HTML-like element
Expand Down

0 comments on commit 5018ce8

Please sign in to comment.