Skip to content

Commit

Permalink
domPassthrough (#17)
Browse files Browse the repository at this point in the history
* domPassthrough

* Version bump
  • Loading branch information
John Richard Chipps-Harding committed Nov 22, 2022
1 parent 99cc192 commit 12a8a25
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 8 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,27 @@ const Link = styled(NextLink, {
});
```

### DOM Shielding

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.

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.

```tsx
import { CSSComponentPropType } from "@phntms/css-components";
import css from "./styles.module.css";

const Input = styled("input", {
css: css.root,
variants: {
readOnly: {
true: css.disabledStyle,
},
},
domPassthrough: ["readOnly"],
});
```

### Type Helper

We have included a helper that allows you to access the types of the variants you have defined.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@phntms/css-components",
"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.",
"version": "0.1.2",
"version": "0.1.3",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"homepage": "https://github.com/phantomstudios/css-components#readme",
Expand Down
21 changes: 17 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,22 @@ export const styled = <
const mergedProps = { ...config?.defaultVariants, ...props } as {
[key: string]: string;
};

// Initialize variables to store the new props and styles
const componentProps: { [key: string]: unknown } = {};
const componentStyles: string[] = [];

// Pass through an existing className if it exists
if (props.className) componentStyles.push(props.className);
if (mergedProps.className) componentStyles.push(mergedProps.className);

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

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

// Apply any variant styles
Object.keys(mergedProps).forEach((key) => {
// Apply any variant styles
if (config?.variants && config.variants.hasOwnProperty(key)) {
const variant = config.variants[key as keyof typeof config.variants];
if (variant && variant.hasOwnProperty(mergedProps[key])) {
Expand All @@ -45,9 +46,21 @@ export const styled = <
] as cssType;
componentStyles.push(flattenCss(selector));
}
} else {
componentProps[key] = props[key];
}

const isDomNode = typeof element === "string";
const isVariant =
config?.variants && config.variants.hasOwnProperty(key);

// Only pass through the prop if it's not a variant or been told to pass through
if (
isDomNode &&
isVariant &&
!config?.domPassthrough?.includes(key as keyof V)
)
return;

componentProps[key] = mergedProps[key];
});

// Apply any compound variant styles
Expand Down
1 change: 1 addition & 0 deletions src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export interface CSSComponentConfig<V> {
defaultVariants?: {
[Property in keyof V]?: BooleanIfStringBoolean<keyof V[Property]>;
};
domPassthrough?: (keyof V)[];
}

/**
Expand Down
152 changes: 151 additions & 1 deletion test/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe("Basic functionality", () => {
it("should provide typescript support for built in types", async () => {
const Input = styled("input");
const onChange = jest.fn();
const { container } = render(<Input value={"test"} onChange={onChange} />);
const { container } = render(<Input value="test" onChange={onChange} />);
expect(container.firstChild).toHaveAttribute("value", "test");
});

Expand Down Expand Up @@ -293,3 +293,153 @@ describe("supports more exotic setups", () => {
expect(container.firstChild).toHaveClass("primary");
});
});

describe("supports inheritance", () => {
it("should handle component composition", async () => {
const BaseButton = styled("button", {
css: "baseButton",
variants: {
big: { true: "big" },
},
});

const CheckoutButton = styled(BaseButton, {
css: "checkoutButton",
});

const { container } = render(<CheckoutButton big />);

expect(container.firstChild?.nodeName).toEqual("BUTTON");
expect(container.firstChild).toHaveClass("baseButton");
expect(container.firstChild).toHaveClass("checkoutButton");
expect(container.firstChild).toHaveClass("big");
});

it("should handle component composition when overriding variants", async () => {
const BaseButton = styled("button", {
css: "baseButton",
variants: {
big: { true: "big" },
},
});

const CheckoutButton = styled(BaseButton, {
css: "checkoutButton",
variants: {
big: { true: "checkoutButtonBig" },
},
});

const { container } = render(<CheckoutButton big />);

expect(container.firstChild?.nodeName).toEqual("BUTTON");
expect(container.firstChild).toHaveClass("baseButton");
expect(container.firstChild).toHaveClass("checkoutButton");
expect(container.firstChild).toHaveClass("big");
expect(container.firstChild).toHaveClass("checkoutButtonBig");
});

it("should handle component composition with default variants", async () => {
const BaseButton = styled("button", {
css: "baseButton",
variants: {
big: { true: "baseButtonBig" },
theme: {
primary: "baseButtonPrimary",
secondary: "baseButtonSecondary",
},
anotherBool: { true: "baseButtonAnotherBool" },
},
defaultVariants: {
big: true,
theme: "primary",
anotherBool: true,
},
});

const CheckoutButton = styled(BaseButton, {
css: "checkoutButton",
variants: {
big: { true: "checkoutButtonBig" },
theme: {
primary: "checkoutButtonPrimary",
secondary: "checkoutButtonSecondary",
},
anotherBool: { true: "checkoutButtonAnotherBool" },
},
defaultVariants: {
big: true,
anotherBool: true,
theme: "primary",
},
});

const { container } = render(<CheckoutButton />);

expect(container.firstChild?.nodeName).toEqual("BUTTON");

expect(container.firstChild).toHaveClass("baseButton");
expect(container.firstChild).toHaveClass("baseButtonBig");
expect(container.firstChild).toHaveClass("baseButtonPrimary");
expect(container.firstChild).toHaveClass("baseButtonAnotherBool");

expect(container.firstChild).toHaveClass("checkoutButton");
expect(container.firstChild).toHaveClass("checkoutButtonBig");
expect(container.firstChild).toHaveClass("checkoutButtonPrimary");
expect(container.firstChild).toHaveClass("checkoutButtonAnotherBool");
});

it("variant props should not propagate to the DOM by default", async () => {
const Input = styled("input", {
css: "input",
variants: {
big: { true: "big" },
},
});

const { container } = render(<Input big />);

expect(container.firstChild).toHaveClass("big");
expect(container.firstChild).not.toHaveAttribute("big");
});

it("css components should not block intrinsic props that are not styled", async () => {
const Input = styled("input");
const onChange = jest.fn();
const { container } = render(<Input value="test" onChange={onChange} />);
expect(container.firstChild).toHaveAttribute("value", "test");
});

it("variants should allow intrinsic props to pass through to the DOM", async () => {
const Input = styled("input", {
css: "input",
variants: {
type: { text: "textInput" },
},
domPassthrough: ["type"],
});

const { container } = render(<Input type="text" />);

expect(container.firstChild?.nodeName).toEqual("INPUT");
expect(container.firstChild).toHaveClass("textInput");
expect(container.firstChild).toHaveAttribute("type", "text");
});

it("variants should allow intrinsic bool props to pass through to the DOM", async () => {
const Input = styled("input", {
css: "input",
variants: {
readOnly: { true: "readOnly" },
},
domPassthrough: ["readOnly"],
});

const { container } = render(<Input type="text" readOnly />);

expect(container.firstChild?.nodeName).toEqual("INPUT");

expect(container.firstChild).toHaveClass("readOnly");
expect(container.firstChild).toHaveAttribute("readOnly");
});
});

0 comments on commit 12a8a25

Please sign in to comment.