Skip to content

Commit 12a8a25

Browse files
author
John Richard Chipps-Harding
authored
domPassthrough (#17)
* domPassthrough * Version bump
1 parent 99cc192 commit 12a8a25

File tree

6 files changed

+193
-8
lines changed

6 files changed

+193
-8
lines changed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,27 @@ const Link = styled(NextLink, {
216216
});
217217
```
218218

219+
### DOM Shielding
220+
221+
By default variant values do not end up propagating to the final DOM element. This is to stop React specific runtime errors from occurring. If you do indeed want to pass a variant value to the DOM element, you can use the `domPassthrough` option.
222+
223+
In the following example, `readOnly` is an intrinsic HTML attribute that we both want to style, but also continue to pass through to the DOM element.
224+
225+
```tsx
226+
import { CSSComponentPropType } from "@phntms/css-components";
227+
import css from "./styles.module.css";
228+
229+
const Input = styled("input", {
230+
css: css.root,
231+
variants: {
232+
readOnly: {
233+
true: css.disabledStyle,
234+
},
235+
},
236+
domPassthrough: ["readOnly"],
237+
});
238+
```
239+
219240
### Type Helper
220241

221242
We have included a helper that allows you to access the types of the variants you have defined.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@phntms/css-components",
33
"description": "At its core, css-components is a simple wrapper around standard CSS. It allows you to write your CSS how you wish then compose them into a component ready to be used in React.",
4-
"version": "0.1.2",
4+
"version": "0.1.3",
55
"main": "lib/index.js",
66
"types": "lib/index.d.ts",
77
"homepage": "https://github.com/phantomstudios/css-components#readme",

src/index.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,22 @@ export const styled = <
2222
const mergedProps = { ...config?.defaultVariants, ...props } as {
2323
[key: string]: string;
2424
};
25+
2526
// Initialize variables to store the new props and styles
2627
const componentProps: { [key: string]: unknown } = {};
2728
const componentStyles: string[] = [];
2829

2930
// Pass through an existing className if it exists
30-
if (props.className) componentStyles.push(props.className);
31+
if (mergedProps.className) componentStyles.push(mergedProps.className);
3132

3233
// Add the base style(s)
3334
if (config?.css) componentStyles.push(flattenCss(config.css));
3435

3536
// Pass through the ref
3637
if (ref) componentProps.ref = ref;
3738

38-
// Apply any variant styles
3939
Object.keys(mergedProps).forEach((key) => {
40+
// Apply any variant styles
4041
if (config?.variants && config.variants.hasOwnProperty(key)) {
4142
const variant = config.variants[key as keyof typeof config.variants];
4243
if (variant && variant.hasOwnProperty(mergedProps[key])) {
@@ -45,9 +46,21 @@ export const styled = <
4546
] as cssType;
4647
componentStyles.push(flattenCss(selector));
4748
}
48-
} else {
49-
componentProps[key] = props[key];
5049
}
50+
51+
const isDomNode = typeof element === "string";
52+
const isVariant =
53+
config?.variants && config.variants.hasOwnProperty(key);
54+
55+
// Only pass through the prop if it's not a variant or been told to pass through
56+
if (
57+
isDomNode &&
58+
isVariant &&
59+
!config?.domPassthrough?.includes(key as keyof V)
60+
)
61+
return;
62+
63+
componentProps[key] = mergedProps[key];
5164
});
5265

5366
// Apply any compound variant styles

src/type.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export interface CSSComponentConfig<V> {
6767
defaultVariants?: {
6868
[Property in keyof V]?: BooleanIfStringBoolean<keyof V[Property]>;
6969
};
70+
domPassthrough?: (keyof V)[];
7071
}
7172

7273
/**

test/index.test.tsx

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ describe("Basic functionality", () => {
5252
it("should provide typescript support for built in types", async () => {
5353
const Input = styled("input");
5454
const onChange = jest.fn();
55-
const { container } = render(<Input value={"test"} onChange={onChange} />);
55+
const { container } = render(<Input value="test" onChange={onChange} />);
5656
expect(container.firstChild).toHaveAttribute("value", "test");
5757
});
5858

@@ -293,3 +293,153 @@ describe("supports more exotic setups", () => {
293293
expect(container.firstChild).toHaveClass("primary");
294294
});
295295
});
296+
297+
describe("supports inheritance", () => {
298+
it("should handle component composition", async () => {
299+
const BaseButton = styled("button", {
300+
css: "baseButton",
301+
variants: {
302+
big: { true: "big" },
303+
},
304+
});
305+
306+
const CheckoutButton = styled(BaseButton, {
307+
css: "checkoutButton",
308+
});
309+
310+
const { container } = render(<CheckoutButton big />);
311+
312+
expect(container.firstChild?.nodeName).toEqual("BUTTON");
313+
expect(container.firstChild).toHaveClass("baseButton");
314+
expect(container.firstChild).toHaveClass("checkoutButton");
315+
expect(container.firstChild).toHaveClass("big");
316+
});
317+
318+
it("should handle component composition when overriding variants", async () => {
319+
const BaseButton = styled("button", {
320+
css: "baseButton",
321+
variants: {
322+
big: { true: "big" },
323+
},
324+
});
325+
326+
const CheckoutButton = styled(BaseButton, {
327+
css: "checkoutButton",
328+
variants: {
329+
big: { true: "checkoutButtonBig" },
330+
},
331+
});
332+
333+
const { container } = render(<CheckoutButton big />);
334+
335+
expect(container.firstChild?.nodeName).toEqual("BUTTON");
336+
expect(container.firstChild).toHaveClass("baseButton");
337+
expect(container.firstChild).toHaveClass("checkoutButton");
338+
expect(container.firstChild).toHaveClass("big");
339+
expect(container.firstChild).toHaveClass("checkoutButtonBig");
340+
});
341+
342+
it("should handle component composition with default variants", async () => {
343+
const BaseButton = styled("button", {
344+
css: "baseButton",
345+
variants: {
346+
big: { true: "baseButtonBig" },
347+
theme: {
348+
primary: "baseButtonPrimary",
349+
secondary: "baseButtonSecondary",
350+
},
351+
anotherBool: { true: "baseButtonAnotherBool" },
352+
},
353+
defaultVariants: {
354+
big: true,
355+
theme: "primary",
356+
anotherBool: true,
357+
},
358+
});
359+
360+
const CheckoutButton = styled(BaseButton, {
361+
css: "checkoutButton",
362+
variants: {
363+
big: { true: "checkoutButtonBig" },
364+
theme: {
365+
primary: "checkoutButtonPrimary",
366+
secondary: "checkoutButtonSecondary",
367+
},
368+
anotherBool: { true: "checkoutButtonAnotherBool" },
369+
},
370+
defaultVariants: {
371+
big: true,
372+
anotherBool: true,
373+
theme: "primary",
374+
},
375+
});
376+
377+
const { container } = render(<CheckoutButton />);
378+
379+
expect(container.firstChild?.nodeName).toEqual("BUTTON");
380+
381+
expect(container.firstChild).toHaveClass("baseButton");
382+
expect(container.firstChild).toHaveClass("baseButtonBig");
383+
expect(container.firstChild).toHaveClass("baseButtonPrimary");
384+
expect(container.firstChild).toHaveClass("baseButtonAnotherBool");
385+
386+
expect(container.firstChild).toHaveClass("checkoutButton");
387+
expect(container.firstChild).toHaveClass("checkoutButtonBig");
388+
expect(container.firstChild).toHaveClass("checkoutButtonPrimary");
389+
expect(container.firstChild).toHaveClass("checkoutButtonAnotherBool");
390+
});
391+
392+
it("variant props should not propagate to the DOM by default", async () => {
393+
const Input = styled("input", {
394+
css: "input",
395+
variants: {
396+
big: { true: "big" },
397+
},
398+
});
399+
400+
const { container } = render(<Input big />);
401+
402+
expect(container.firstChild).toHaveClass("big");
403+
expect(container.firstChild).not.toHaveAttribute("big");
404+
});
405+
406+
it("css components should not block intrinsic props that are not styled", async () => {
407+
const Input = styled("input");
408+
const onChange = jest.fn();
409+
const { container } = render(<Input value="test" onChange={onChange} />);
410+
expect(container.firstChild).toHaveAttribute("value", "test");
411+
});
412+
413+
it("variants should allow intrinsic props to pass through to the DOM", async () => {
414+
const Input = styled("input", {
415+
css: "input",
416+
variants: {
417+
type: { text: "textInput" },
418+
},
419+
domPassthrough: ["type"],
420+
});
421+
422+
const { container } = render(<Input type="text" />);
423+
424+
expect(container.firstChild?.nodeName).toEqual("INPUT");
425+
expect(container.firstChild).toHaveClass("textInput");
426+
expect(container.firstChild).toHaveAttribute("type", "text");
427+
});
428+
429+
it("variants should allow intrinsic bool props to pass through to the DOM", async () => {
430+
const Input = styled("input", {
431+
css: "input",
432+
variants: {
433+
readOnly: { true: "readOnly" },
434+
},
435+
domPassthrough: ["readOnly"],
436+
});
437+
438+
const { container } = render(<Input type="text" readOnly />);
439+
440+
expect(container.firstChild?.nodeName).toEqual("INPUT");
441+
442+
expect(container.firstChild).toHaveClass("readOnly");
443+
expect(container.firstChild).toHaveAttribute("readOnly");
444+
});
445+
});

0 commit comments

Comments
 (0)