Skip to content

Commit 5018ce8

Browse files
committedAug 22, 2024
fix(tailwind): className manipulation for component props (#1556)
1 parent 6d27a20 commit 5018ce8

File tree

8 files changed

+55
-29
lines changed

8 files changed

+55
-29
lines changed
 

‎.changeset/many-donuts-retire.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@react-email/tailwind": minor
3+
---
4+
5+
- Add support for proper `className` manipulation
6+
- Make inline styles override Tailwind styles.

‎packages/react-email/src/cli/commands/testing/__snapshots__/export.spec.ts.snap

+10-10
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,30 @@ exports[`email export 1`] = `
1414
</div>
1515
1616
<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">
17-
<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">
17+
<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">
1818
<tbody>
1919
<tr style="width:100%">
2020
<td>
2121
<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-top:32px">
2222
<tbody>
2323
<tr>
24-
<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>
24+
<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>
2525
</tr>
2626
</tbody>
2727
</table>
2828
<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>
29-
<p style="font-size:14px;line-height:24px;margin:16px 0;color:rgb(0,0,0)">Hello <!-- -->,</p>
30-
<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>
29+
<p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin:16px 0">Hello <!-- -->,</p>
30+
<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>
3131
<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation">
3232
<tbody>
3333
<tr>
3434
<td>
3535
<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation">
3636
<tbody style="width:100%">
3737
<tr style="width:100%">
38-
<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>
38+
<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>
3939
<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>
40-
<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>
40+
<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>
4141
</tr>
4242
</tbody>
4343
</table>
@@ -48,13 +48,13 @@ exports[`email export 1`] = `
4848
<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:center;margin-top:32px;margin-bottom:32px">
4949
<tbody>
5050
<tr>
51-
<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>
51+
<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>
5252
</tr>
5353
</tbody>
5454
</table>
55-
<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>
56-
<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" />
57-
<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>
55+
<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>
56+
<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" />
57+
<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>
5858
</td>
5959
</tr>
6060
</tbody>

‎packages/react-email/src/utils/__snapshots__/get-email-component.spec.ts.snap

+1-1
Large diffs are not rendered by default.

‎packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@ If you do already have a <head> element at some depth, please file a bug https:/
1212
1313
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>"`;
1414
15-
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"`;
15+
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"`;
1616
1717
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>"`;
1818
19+
exports[`Tailwind component > should not override inline styles with Tailwind styles 1`] = `"<div style=\\"background-color:red;font-size:12px\\"></div>"`;
20+
1921
exports[`Tailwind component > should work with Heading component 1`] = `"Hello<h1>My testing heading</h1>friends"`;
22+
23+
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>"`;

‎packages/tailwind/src/tailwind.spec.tsx

+16-4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,20 @@ describe("Tailwind component", () => {
2424
expect(actualOutput).toMatchSnapshot();
2525
});
2626

27+
it("should work with class manipulation done on components", () => {
28+
const MyComponnt = (props: { className?: string }) => {
29+
return <div className={`${props.className} bg-red-500`} />;
30+
};
31+
32+
expect(
33+
render(
34+
<Tailwind>
35+
<MyComponnt className="text-blue-400" />
36+
</Tailwind>,
37+
),
38+
).toMatchSnapshot();
39+
});
40+
2741
describe("Inline styles", () => {
2842
it("should render children with inline Tailwind styles", () => {
2943
const actualOutput = render(
@@ -178,7 +192,7 @@ describe("Tailwind component", () => {
178192
);
179193
});
180194

181-
it("should override inline styles with Tailwind styles", () => {
195+
it("should not override inline styles with Tailwind styles", () => {
182196
const actualOutput = render(
183197
<Tailwind>
184198
<div
@@ -188,9 +202,7 @@ describe("Tailwind component", () => {
188202
</Tailwind>,
189203
);
190204

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

196208
it("should override component styles with Tailwind styles", () => {

‎packages/tailwind/src/tailwind.tsx

+13-11
Original file line numberDiff line numberDiff line change
@@ -101,25 +101,27 @@ export const Tailwind: React.FC<TailwindProps> = ({ children, config }) => {
101101
if (element.props.className) {
102102
const { styles, residualClassName } = inline(element.props.className);
103103
propsToOverwrite.style = {
104-
...element.props.style,
105104
...styles,
105+
...element.props.style,
106106
};
107-
if (residualClassName.trim().length > 0) {
108-
propsToOverwrite.className = residualClassName;
109-
/*
107+
if (!isComponent(element)) {
108+
if (residualClassName.trim().length > 0) {
109+
propsToOverwrite.className = residualClassName;
110+
/*
110111
We sanitize only the class names of Tailwind classes that we are not going to inline
111112
to avoid unpredictable behavior on the user's code. If we did sanitize all class names
112113
a user-defined class could end up also being sanitized which would lead to unexpected
113114
behavior and bugs that are hard to track.
114115
*/
115-
for (const singleClass of nonInlinableClasses) {
116-
propsToOverwrite.className = propsToOverwrite.className.replace(
117-
singleClass,
118-
sanitizeClassName(singleClass),
119-
);
116+
for (const singleClass of nonInlinableClasses) {
117+
propsToOverwrite.className = propsToOverwrite.className.replace(
118+
singleClass,
119+
sanitizeClassName(singleClass),
120+
);
121+
}
122+
} else {
123+
propsToOverwrite.className = undefined;
120124
}
121-
} else {
122-
propsToOverwrite.className = undefined;
123125
}
124126
}
125127

Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3-
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>"`;
3+
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>"`;

‎packages/tailwind/src/utils/quick-safe-render-to-string.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ export const quickSafeRenderToString = (element: React.ReactNode): string => {
3939
: (type as React.FC<Props>);
4040
// If the element is a component (function component), render it
4141
const componentRenderingResults = functionComponent(props);
42-
return quickSafeRenderToString(componentRenderingResults);
42+
return `<${functionComponent.name}>${quickSafeRenderToString(
43+
componentRenderingResults,
44+
)}</${functionComponent.name}>`;
4345
}
4446

4547
// Regular HTML-like element

0 commit comments

Comments
 (0)
Please sign in to comment.