@yak/react 0.0.0-stage → 10.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +102 -2
- package/dist/context/baseContext.cjs +9 -0
- package/dist/context/baseContext.cjs.map +1 -0
- package/dist/context/baseContext.d.cts +6 -0
- package/dist/context/baseContext.d.ts +6 -0
- package/dist/context/baseContext.js +8 -0
- package/dist/context/baseContext.js.map +1 -0
- package/dist/context/index.cjs +45 -0
- package/dist/context/index.cjs.map +1 -0
- package/dist/context/index.d.cts +21 -0
- package/dist/context/index.d.ts +21 -0
- package/dist/context/index.js +15 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/index.server.cjs +47 -0
- package/dist/context/index.server.cjs.map +1 -0
- package/dist/context/index.server.d.cts +10 -0
- package/dist/context/index.server.d.ts +10 -0
- package/dist/context/index.server.js +17 -0
- package/dist/context/index.server.js.map +1 -0
- package/dist/cross-file-resolver/index.d.ts +199 -0
- package/dist/cross-file-resolver/index.js +535 -0
- package/dist/cross-file-resolver/index.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +320 -0
- package/dist/index.d.ts +320 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.cjs +544 -0
- package/dist/internal.cjs.map +1 -0
- package/dist/internal.d.cts +452 -0
- package/dist/internal.d.ts +452 -0
- package/dist/internal.js +363 -0
- package/dist/internal.js.map +1 -0
- package/dist/isolated-source-eval/index.d.ts +30 -0
- package/dist/isolated-source-eval/index.js +317 -0
- package/dist/isolated-source-eval/index.js.map +1 -0
- package/dist/isolated-source-eval/worker.js +59 -0
- package/dist/isolated-source-eval/worker.js.map +1 -0
- package/dist/jsx-dev-runtime.cjs +2 -0
- package/dist/jsx-dev-runtime.cjs.map +1 -0
- package/dist/jsx-dev-runtime.d.cts +7 -0
- package/dist/jsx-dev-runtime.d.ts +7 -0
- package/dist/jsx-dev-runtime.js +2 -0
- package/dist/jsx-dev-runtime.js.map +1 -0
- package/dist/jsx-runtime.cjs +2 -0
- package/dist/jsx-runtime.cjs.map +1 -0
- package/dist/jsx-runtime.d.cts +38 -0
- package/dist/jsx-runtime.d.ts +38 -0
- package/dist/jsx-runtime.js +2 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/loaders/turbo-loader.cjs +803 -0
- package/dist/loaders/turbo-loader.cjs.map +1 -0
- package/dist/loaders/turbo-loader.d.cts +15 -0
- package/dist/loaders/vite-plugin.d.ts +111 -0
- package/dist/loaders/vite-plugin.js +878 -0
- package/dist/loaders/vite-plugin.js.map +1 -0
- package/dist/loaders/webpack-loader.cjs +697 -0
- package/dist/loaders/webpack-loader.cjs.map +1 -0
- package/dist/loaders/webpack-loader.d.cts +12 -0
- package/dist/rsbuild/index.d.ts +29 -0
- package/dist/rsbuild/index.js +52 -0
- package/dist/rsbuild/index.js.map +1 -0
- package/dist/static/chunk-DK3Fl9T5.js +1 -0
- package/dist/static/index.cjs +2 -0
- package/dist/static/index.cjs.map +1 -0
- package/dist/static/index.d.cts +28 -0
- package/dist/static/index.d.ts +28 -0
- package/dist/static/index.js +2 -0
- package/dist/static/index.js.map +1 -0
- package/dist/withYak/index.cjs +208 -0
- package/dist/withYak/index.cjs.map +1 -0
- package/dist/withYak/index.d.cts +121 -0
- package/dist/withYak/index.d.ts +121 -0
- package/dist/withYak/index.js +177 -0
- package/dist/withYak/index.js.map +1 -0
- package/loaders/lib/resolveCrossFileSelectors.ts +162 -0
- package/loaders/turbo-evaluator.ts +134 -0
- package/loaders/turbo-loader.ts +180 -0
- package/loaders/vite-plugin.ts +28 -0
- package/loaders/webpack-loader.ts +44 -0
- package/package.json +214 -3
- package/rsbuild/index.ts +92 -0
- package/runtime/__tests__/atoms.test.tsx +48 -0
- package/runtime/__tests__/attrs.test.tsx +490 -0
- package/runtime/__tests__/cssLiteral.test.tsx +503 -0
- package/runtime/__tests__/cssProp.test.tsx +392 -0
- package/runtime/__tests__/cssPropTest.tsx +227 -0
- package/runtime/__tests__/foldedJsxParity.test.tsx +117 -0
- package/runtime/__tests__/styled.test.tsx +653 -0
- package/runtime/__tests__/tsconfig.json +18 -0
- package/runtime/__tests__/typeTest.tsx +374 -0
- package/runtime/atoms.tsx +36 -0
- package/runtime/context/README.md +84 -0
- package/runtime/context/baseContext.tsx +5 -0
- package/runtime/context/index.server.tsx +22 -0
- package/runtime/context/index.tsx +36 -0
- package/runtime/cssLiteral.tsx +214 -0
- package/runtime/globalStyle.tsx +54 -0
- package/runtime/index.ts +37 -0
- package/runtime/internal.ts +42 -0
- package/runtime/internals/mergeClassNames.ts +23 -0
- package/runtime/internals/mergeCssProp.ts +69 -0
- package/runtime/internals/propMarkers.ts +20 -0
- package/runtime/internals/unitPostFix.ts +34 -0
- package/runtime/jsx-dev-runtime.ts +6 -0
- package/runtime/jsx-runtime.ts +48 -0
- package/runtime/keyframes.tsx +30 -0
- package/runtime/mocks/cssLiteral.ts +74 -0
- package/runtime/mocks/globalStyle.ts +14 -0
- package/runtime/mocks/keyframes.ts +29 -0
- package/runtime/mocks/styled.ts +8 -0
- package/runtime/publicStyledApi.ts +159 -0
- package/runtime/styled.tsx +372 -0
- package/runtime/styledDom.tsx +139 -0
- package/withYak/allow-global-css.ts +94 -0
- package/withYak/index.ts +282 -0
package/rsbuild/index.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import type { RsbuildPlugin } from "@rsbuild/core";
|
|
3
|
+
import {
|
|
4
|
+
buildYakPluginOptions,
|
|
5
|
+
resolveYakContext,
|
|
6
|
+
type YakConfigOptions,
|
|
7
|
+
} from "../withYak/index.js";
|
|
8
|
+
|
|
9
|
+
// Rspack's loader context is webpack-compatible but does NOT implement
|
|
10
|
+
// `this.loadModule` (the webpack API the webpack-loader relies on). It does
|
|
11
|
+
// provide `this.fs.readFile`, `this.getResolve` and `this.addDependency` — which
|
|
12
|
+
// is exactly what the turbopack loader uses — so the turbopack loader runs as-is
|
|
13
|
+
// on Rspack. The compiled loader lives next to this file under dist/
|
|
14
|
+
// (dist/rsbuild/index.js and dist/loaders/turbo-loader.cjs are siblings).
|
|
15
|
+
const rspackLoaderPath = fileURLToPath(new URL("../loaders/turbo-loader.cjs", import.meta.url));
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Rsbuild plugin for @yak/react.
|
|
19
|
+
*
|
|
20
|
+
* Wires the yak-swc transform as an Rspack `pre` loader that runs the
|
|
21
|
+
* compile-time CSS extraction in-loader and inlines the result as a
|
|
22
|
+
* `data:text/css;base64,` import (the same strategy as the Turbopack loader).
|
|
23
|
+
* Running it as a `pre` loader keeps yak-swc out of Rsbuild's own
|
|
24
|
+
* `builtin:swc-loader`, so it composes cleanly with that pass (TS/JSX, React
|
|
25
|
+
* Fast Refresh, and the React Compiler).
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* // rsbuild.config.ts
|
|
30
|
+
* import { defineConfig } from "@rsbuild/core";
|
|
31
|
+
* import { pluginReact } from "@rsbuild/plugin-react";
|
|
32
|
+
* import { pluginYak } from "@yak/react/rsbuild";
|
|
33
|
+
*
|
|
34
|
+
* export default defineConfig({
|
|
35
|
+
* plugins: [pluginReact(), pluginYak()],
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function pluginYak(yakOptions: YakConfigOptions = {}): RsbuildPlugin {
|
|
40
|
+
return {
|
|
41
|
+
name: "@yak/react",
|
|
42
|
+
setup(api) {
|
|
43
|
+
api.modifyRspackConfig((config) => {
|
|
44
|
+
const rootContext = api.context.rootPath;
|
|
45
|
+
const yakPluginOptions = {
|
|
46
|
+
...buildYakPluginOptions(yakOptions, rootContext),
|
|
47
|
+
importMode: {
|
|
48
|
+
value: "data:text/css;base64,",
|
|
49
|
+
transpilation: "Css",
|
|
50
|
+
encoding: "Base64",
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
config.module ??= {};
|
|
55
|
+
config.module.rules ??= [];
|
|
56
|
+
config.module.rules.push(
|
|
57
|
+
{
|
|
58
|
+
test: /\.(c|m)?[jt]sx?$/,
|
|
59
|
+
exclude: /[\\/]node_modules[\\/]/,
|
|
60
|
+
enforce: "pre",
|
|
61
|
+
use: [
|
|
62
|
+
{
|
|
63
|
+
loader: rspackLoaderPath,
|
|
64
|
+
options: { yakOptions, yakPluginOptions },
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
// The extracted CSS is inlined as a `data:text/css;base64,` import.
|
|
69
|
+
// Rspack parses unknown data URIs as JavaScript, so tell it these are
|
|
70
|
+
// CSS modules. `css/auto` keeps Rspack's native CSS handling for them.
|
|
71
|
+
{
|
|
72
|
+
mimetype: "text/css",
|
|
73
|
+
type: "css/auto",
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Custom context file (`yak.context.{ts,tsx,js,jsx}`) for server-aware
|
|
78
|
+
// theming, mirroring the webpack/turbopack integrations.
|
|
79
|
+
const yakContext = resolveYakContext(yakOptions.contextPath, rootContext);
|
|
80
|
+
if (yakContext) {
|
|
81
|
+
config.resolve ??= {};
|
|
82
|
+
config.resolve.alias = {
|
|
83
|
+
...config.resolve.alias,
|
|
84
|
+
"@yak/react/context/baseContext": yakContext,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type { YakConfigOptions };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { atoms } from "../atoms";
|
|
3
|
+
import { ClassNames, css } from "../cssLiteral";
|
|
4
|
+
import type { RuntimeStyleProcessor } from "../publicStyledApi";
|
|
5
|
+
|
|
6
|
+
const classNamesOf = (processor: unknown, props: Record<string, unknown> = {}) => {
|
|
7
|
+
const classNames = new ClassNames();
|
|
8
|
+
(processor as RuntimeStyleProcessor<unknown>)(props, classNames, {});
|
|
9
|
+
return classNames.value;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe("atoms", () => {
|
|
13
|
+
it("collects static classes, however they are grouped", () => {
|
|
14
|
+
expect(classNamesOf(atoms("a b c"))).toBe("a b c");
|
|
15
|
+
expect(classNamesOf(atoms("a", "b", "c"))).toBe("a b c");
|
|
16
|
+
expect(classNamesOf(atoms("a b", "c"))).toBe("a b c");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("skips falsy atoms", () => {
|
|
20
|
+
expect(classNamesOf(atoms("a", false, "b"))).toBe("a b");
|
|
21
|
+
expect(classNamesOf(atoms(false))).toBe("");
|
|
22
|
+
expect(classNamesOf(atoms())).toBe("");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("runs dynamic atoms after the static ones", () => {
|
|
26
|
+
const processor = atoms<{ $active?: boolean }>("a", (props, classNames) => {
|
|
27
|
+
if (props.$active) classNames.add("active");
|
|
28
|
+
});
|
|
29
|
+
expect(classNamesOf(processor, { $active: true })).toBe("a active");
|
|
30
|
+
expect(classNamesOf(processor, { $active: false })).toBe("a");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("keeps classes a dynamic atom removes removable", () => {
|
|
34
|
+
const processor = atoms("a b", (_props, classNames) => {
|
|
35
|
+
classNames.delete("a");
|
|
36
|
+
});
|
|
37
|
+
expect(classNamesOf(processor)).toBe("b");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("is static on its own but dynamic once css() wraps it", () => {
|
|
41
|
+
// styled`` compiles to css("yak123", atoms(...)) and gates its fast path on
|
|
42
|
+
// the outer processor, so atoms' own $dynamic is not read today. Pinned to
|
|
43
|
+
// catch a fold of static atoms that would make the fast path reachable
|
|
44
|
+
expect((atoms("a") as unknown as { $dynamic: boolean }).$dynamic).toBe(false);
|
|
45
|
+
// @ts-expect-error calling with a class name string mimics the compiled css form
|
|
46
|
+
expect((css("yak123", atoms("a")) as unknown as { $dynamic: boolean }).$dynamic).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import React, { FunctionComponent } from "react";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import { beforeEach, expect, describe, it, vi } from "vitest";
|
|
4
|
+
import { YakThemeProvider } from "../context";
|
|
5
|
+
import { styled as styledFn } from "../styled";
|
|
6
|
+
|
|
7
|
+
// This transform is usually done by the SWC plugin.
|
|
8
|
+
// However this `attrs.test.tsx` does not compile
|
|
9
|
+
// the code before testing
|
|
10
|
+
const styled = Object.assign(styledFn, {
|
|
11
|
+
div: styledFn("div"),
|
|
12
|
+
h1: styledFn("h1"),
|
|
13
|
+
button: styledFn("button"),
|
|
14
|
+
a: styledFn("a"),
|
|
15
|
+
p: styledFn("p"),
|
|
16
|
+
span: styledFn("span"),
|
|
17
|
+
}) as typeof styledFn;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.spyOn(console, "warn");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
type DataAttributes = { [key: `data-${string}`]: any };
|
|
24
|
+
|
|
25
|
+
const getSnapshot = (element: React.ReactElement) => render(element).container.firstChild;
|
|
26
|
+
|
|
27
|
+
it("works fine with an empty object", () => {
|
|
28
|
+
const Comp = styled.div.attrs({})``;
|
|
29
|
+
expect(getSnapshot(<Comp />)).toMatchInlineSnapshot(`<div />`);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("works fine with a function that returns an empty object", () => {
|
|
33
|
+
const Comp = styled.div.attrs(() => ({}))``;
|
|
34
|
+
expect(getSnapshot(<Comp />)).toMatchInlineSnapshot(`<div />`);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("pass a simple attr via object", () => {
|
|
38
|
+
const Comp = styled.button.attrs({
|
|
39
|
+
type: "button",
|
|
40
|
+
})``;
|
|
41
|
+
expect(getSnapshot(<Comp />)).toMatchInlineSnapshot(`
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
/>
|
|
45
|
+
`);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("pass a simple attr via function with object return", () => {
|
|
49
|
+
const Comp = styled.button.attrs(() => ({
|
|
50
|
+
type: "button",
|
|
51
|
+
}))``;
|
|
52
|
+
expect(getSnapshot(<Comp />)).toMatchInlineSnapshot(`
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
/>
|
|
56
|
+
`);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("pass a React component", () => {
|
|
60
|
+
const ReactComponent: FunctionComponent = () => <p>React Component</p>;
|
|
61
|
+
|
|
62
|
+
type ButtonProps = {
|
|
63
|
+
component: FunctionComponent;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const Button = ({ component: ChildComponent }: ButtonProps) => (
|
|
67
|
+
<button>
|
|
68
|
+
<ChildComponent />
|
|
69
|
+
</button>
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const Comp = styled(Button).attrs<Partial<ButtonProps>>(() => ({
|
|
73
|
+
component: ReactComponent,
|
|
74
|
+
}))``;
|
|
75
|
+
|
|
76
|
+
expect(getSnapshot(<Comp />)).toMatchInlineSnapshot(`
|
|
77
|
+
<button>
|
|
78
|
+
<p>
|
|
79
|
+
React Component
|
|
80
|
+
</p>
|
|
81
|
+
</button>
|
|
82
|
+
`);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should not call a function passed to attrs as an object value", () => {
|
|
86
|
+
const stub = vi.fn(() => "div");
|
|
87
|
+
|
|
88
|
+
const Comp = styled.button.attrs<{ foo?: typeof stub }>(() => ({
|
|
89
|
+
foo: stub,
|
|
90
|
+
}))``;
|
|
91
|
+
|
|
92
|
+
render(<Comp />);
|
|
93
|
+
|
|
94
|
+
expect(stub).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("pass props to the attr function", () => {
|
|
98
|
+
const Comp = styled.button.attrs<{ $submit?: boolean }>((p) => ({
|
|
99
|
+
type: p.$submit ? "submit" : "button",
|
|
100
|
+
}))``;
|
|
101
|
+
|
|
102
|
+
expect(getSnapshot(<Comp />)).toMatchInlineSnapshot(`
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
/>
|
|
106
|
+
`);
|
|
107
|
+
expect(getSnapshot(<Comp $submit />)).toMatchInlineSnapshot(`
|
|
108
|
+
<button
|
|
109
|
+
type="submit"
|
|
110
|
+
/>
|
|
111
|
+
`);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should replace props with attrs", () => {
|
|
115
|
+
const Comp = styled.button.attrs<{ $submit?: boolean }>((p) => ({
|
|
116
|
+
type: p.$submit ? "submit" : "button",
|
|
117
|
+
tabIndex: 0,
|
|
118
|
+
}))``;
|
|
119
|
+
|
|
120
|
+
expect(getSnapshot(<Comp />)).toMatchInlineSnapshot(`
|
|
121
|
+
<button
|
|
122
|
+
tabindex="0"
|
|
123
|
+
type="button"
|
|
124
|
+
/>
|
|
125
|
+
`);
|
|
126
|
+
expect(getSnapshot(<Comp type="reset" />)).toMatchInlineSnapshot(`
|
|
127
|
+
<button
|
|
128
|
+
tabindex="0"
|
|
129
|
+
type="button"
|
|
130
|
+
/>
|
|
131
|
+
`);
|
|
132
|
+
expect(getSnapshot(<Comp type="reset" tabIndex={-1} />)).toMatchInlineSnapshot(`
|
|
133
|
+
<button
|
|
134
|
+
tabindex="0"
|
|
135
|
+
type="button"
|
|
136
|
+
/>
|
|
137
|
+
`);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should merge className", () => {
|
|
141
|
+
const Comp = styled.div.attrs(() => ({
|
|
142
|
+
className: "meow nya",
|
|
143
|
+
}))``;
|
|
144
|
+
|
|
145
|
+
expect(getSnapshot(<Comp />)).toMatchInlineSnapshot(`
|
|
146
|
+
<div
|
|
147
|
+
class="meow nya"
|
|
148
|
+
/>
|
|
149
|
+
`);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should merge className from folded attrs", () => {
|
|
153
|
+
const Inner = styled.div.attrs({ className: "foo" })``;
|
|
154
|
+
|
|
155
|
+
const Comp = styled(Inner).attrs(() => ({
|
|
156
|
+
className: "meow nya",
|
|
157
|
+
}))``;
|
|
158
|
+
|
|
159
|
+
expect(getSnapshot(<Comp className="something" />)).toMatchInlineSnapshot(`
|
|
160
|
+
<div
|
|
161
|
+
class="something foo meow nya"
|
|
162
|
+
/>
|
|
163
|
+
`);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should merge className even if its a function", () => {
|
|
167
|
+
const Comp = styled.div.attrs<{ $purr?: boolean }>((p) => ({
|
|
168
|
+
className: `meow ${p.$purr ? "purr" : "nya"}`,
|
|
169
|
+
}))``;
|
|
170
|
+
|
|
171
|
+
expect(getSnapshot(<Comp />)).toMatchInlineSnapshot(`
|
|
172
|
+
<div
|
|
173
|
+
class="meow nya"
|
|
174
|
+
/>
|
|
175
|
+
`);
|
|
176
|
+
expect(getSnapshot(<Comp $purr />)).toMatchInlineSnapshot(`
|
|
177
|
+
<div
|
|
178
|
+
class="meow purr"
|
|
179
|
+
/>
|
|
180
|
+
`);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should merge style", () => {
|
|
184
|
+
const Comp = styled.div.attrs(() => ({
|
|
185
|
+
style: { color: "red", background: "blue" },
|
|
186
|
+
}))``;
|
|
187
|
+
|
|
188
|
+
expect(getSnapshot(<Comp style={{ color: "green", borderStyle: "dotted" }} />))
|
|
189
|
+
.toMatchInlineSnapshot(`
|
|
190
|
+
<div
|
|
191
|
+
style="color: red; border-style: dotted; background: blue;"
|
|
192
|
+
/>
|
|
193
|
+
`);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should work with data and aria attributes", () => {
|
|
197
|
+
const Comp = styled.div.attrs<DataAttributes>(() => ({
|
|
198
|
+
"data-foo": "bar",
|
|
199
|
+
"aria-label": "A simple FooBar",
|
|
200
|
+
}))``;
|
|
201
|
+
expect(getSnapshot(<Comp />)).toMatchInlineSnapshot(`
|
|
202
|
+
<div
|
|
203
|
+
aria-label="A simple FooBar"
|
|
204
|
+
data-foo="bar"
|
|
205
|
+
/>
|
|
206
|
+
`);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("merge attrs when inheriting SC", () => {
|
|
210
|
+
let attrsCallCount = 0;
|
|
211
|
+
const Parent = styled.button.attrs(() => {
|
|
212
|
+
// Parent should be called first and only once
|
|
213
|
+
// to behave exactly like styled-components
|
|
214
|
+
expect(attrsCallCount).toEqual(0);
|
|
215
|
+
attrsCallCount++;
|
|
216
|
+
return {
|
|
217
|
+
type: "button",
|
|
218
|
+
tabIndex: 0,
|
|
219
|
+
};
|
|
220
|
+
})``;
|
|
221
|
+
const Child = styled(Parent).attrs(() => {
|
|
222
|
+
// Child should be called second and only once
|
|
223
|
+
// to behave exactly like styled-components
|
|
224
|
+
expect(attrsCallCount).toEqual(1);
|
|
225
|
+
attrsCallCount++;
|
|
226
|
+
return {
|
|
227
|
+
type: "submit",
|
|
228
|
+
};
|
|
229
|
+
})``;
|
|
230
|
+
expect(getSnapshot(<Child />)).toMatchInlineSnapshot(`
|
|
231
|
+
<button
|
|
232
|
+
tabindex="0"
|
|
233
|
+
type="submit"
|
|
234
|
+
/>
|
|
235
|
+
`);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("overrides attrs of the wrapped component and appends its style class", () => {
|
|
239
|
+
// styled(SubmitButton) renders as a single flattened wrapper — this pins
|
|
240
|
+
// that flattening keeps styled-components semantics: the outer attrs win
|
|
241
|
+
// and the outer class comes after the inner one
|
|
242
|
+
// @ts-expect-error calling with a class name string mimics the compiled css form
|
|
243
|
+
const SubmitButton = styled.button.attrs({ type: "submit" })("submit_btn");
|
|
244
|
+
// @ts-expect-error calling with a class name string mimics the compiled css form
|
|
245
|
+
const ResetButton = styled(SubmitButton).attrs({ type: "reset" })("reset_btn");
|
|
246
|
+
|
|
247
|
+
expect(getSnapshot(<SubmitButton />)).toMatchInlineSnapshot(`
|
|
248
|
+
<button
|
|
249
|
+
class="submit_btn"
|
|
250
|
+
type="submit"
|
|
251
|
+
/>
|
|
252
|
+
`);
|
|
253
|
+
expect(getSnapshot(<ResetButton />)).toMatchInlineSnapshot(`
|
|
254
|
+
<button
|
|
255
|
+
class="submit_btn reset_btn"
|
|
256
|
+
type="reset"
|
|
257
|
+
/>
|
|
258
|
+
`);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("pass attrs to style block", () => {
|
|
262
|
+
/* Would be a React Router Link in real life */
|
|
263
|
+
const Comp = styled.a.attrs<DataAttributes>(() => ({
|
|
264
|
+
href: "#",
|
|
265
|
+
"data-active-class-name": "--is-active",
|
|
266
|
+
// @ts-expect-error
|
|
267
|
+
}))("c1", {
|
|
268
|
+
style: {
|
|
269
|
+
"--testVar": (props: any) => props["data-active-class-name"] && "c2",
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
expect(getSnapshot(<Comp />)).toMatchInlineSnapshot(`
|
|
273
|
+
<a
|
|
274
|
+
class="c1"
|
|
275
|
+
data-active-class-name="--is-active"
|
|
276
|
+
href="#"
|
|
277
|
+
style="--testVar: c2;"
|
|
278
|
+
/>
|
|
279
|
+
`);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("should pass through children as a normal prop", () => {
|
|
283
|
+
const Comp = styled.div.attrs(() => ({
|
|
284
|
+
children: "Probably a bad idea",
|
|
285
|
+
}))``;
|
|
286
|
+
expect(getSnapshot(<Comp />)).toMatchInlineSnapshot(`
|
|
287
|
+
<div>
|
|
288
|
+
Probably a bad idea
|
|
289
|
+
</div>
|
|
290
|
+
`);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("should pass through complex children as well", () => {
|
|
294
|
+
const Comp = styled.div.attrs(() => ({
|
|
295
|
+
children: <span>Probably a bad idea</span>,
|
|
296
|
+
}))``;
|
|
297
|
+
expect(getSnapshot(<Comp />)).toMatchInlineSnapshot(`
|
|
298
|
+
<div>
|
|
299
|
+
<span>
|
|
300
|
+
Probably a bad idea
|
|
301
|
+
</span>
|
|
302
|
+
</div>
|
|
303
|
+
`);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("should override children", () => {
|
|
307
|
+
const X = styled.div``;
|
|
308
|
+
type XProps = React.ComponentProps<typeof X>["children"] & React.JSX.Element;
|
|
309
|
+
const Comp = styled.div.attrs(() => ({
|
|
310
|
+
children: <span>Amazing</span>,
|
|
311
|
+
}))``;
|
|
312
|
+
expect(getSnapshot(<Comp>Something else</Comp>)).toMatchInlineSnapshot(`
|
|
313
|
+
<div>
|
|
314
|
+
<span>
|
|
315
|
+
Amazing
|
|
316
|
+
</span>
|
|
317
|
+
</div>
|
|
318
|
+
`);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should shallow merge "style" prop + attr instead of overwriting', () => {
|
|
322
|
+
const Paragraph = styled.p.attrs<{ $fontScale?: number }>((p) => ({
|
|
323
|
+
style: {
|
|
324
|
+
...p.style,
|
|
325
|
+
fontSize: `${p.$fontScale}em`,
|
|
326
|
+
},
|
|
327
|
+
}))<{ $fontScale: number }>``;
|
|
328
|
+
|
|
329
|
+
const Text: FunctionComponent<Partial<React.ComponentProps<typeof Paragraph>>> = (props) => {
|
|
330
|
+
const fontScale = 4;
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<Paragraph $fontScale={fontScale} {...props}>
|
|
334
|
+
{props.children}
|
|
335
|
+
</Paragraph>
|
|
336
|
+
);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const BlueText = styled(Text).attrs(() => ({
|
|
340
|
+
style: {
|
|
341
|
+
color: "blue",
|
|
342
|
+
},
|
|
343
|
+
}))``;
|
|
344
|
+
|
|
345
|
+
expect(getSnapshot(<BlueText>Hello</BlueText>)).toMatchInlineSnapshot(`
|
|
346
|
+
<p
|
|
347
|
+
style="color: blue; font-size: 4em;"
|
|
348
|
+
>
|
|
349
|
+
Hello
|
|
350
|
+
</p>
|
|
351
|
+
`);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("does not pass transient props to HTML element", () => {
|
|
355
|
+
type CompProps = { $textColor: string };
|
|
356
|
+
|
|
357
|
+
const Comp = styled.div<CompProps>`
|
|
358
|
+
color: ${(props) => props.$textColor};
|
|
359
|
+
`;
|
|
360
|
+
|
|
361
|
+
const StyledComp = styled(Comp).attrs<Partial<CompProps>>(() => ({
|
|
362
|
+
$textColor: "red",
|
|
363
|
+
}))``;
|
|
364
|
+
|
|
365
|
+
expect(getSnapshot(<StyledComp />)).toMatchInlineSnapshot(`<div />`);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it.skip('should apply given "as" prop to the progressive type', () => {
|
|
369
|
+
const Comp = styled.div.attrs<{ as?: any }>({ as: "video" as const })``;
|
|
370
|
+
|
|
371
|
+
//@ts-expect-error
|
|
372
|
+
expect(getSnapshot(<Comp loop />)).toMatchInlineSnapshot();
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// our own tests
|
|
376
|
+
it("should remap props", () => {
|
|
377
|
+
const Comp = styled.button.attrs<{ primary?: boolean; $submit?: boolean }>((p) => ({
|
|
378
|
+
type: p.$submit ? "submit" : "button",
|
|
379
|
+
$disabled: p.disabled,
|
|
380
|
+
}))<{ $disabled?: boolean }>``;
|
|
381
|
+
|
|
382
|
+
expect(getSnapshot(<Comp />)).toMatchInlineSnapshot(`
|
|
383
|
+
<button
|
|
384
|
+
type="button"
|
|
385
|
+
/>
|
|
386
|
+
`);
|
|
387
|
+
expect(getSnapshot(<Comp disabled />)).toMatchInlineSnapshot(`
|
|
388
|
+
<button
|
|
389
|
+
disabled=""
|
|
390
|
+
type="button"
|
|
391
|
+
/>
|
|
392
|
+
`);
|
|
393
|
+
expect(getSnapshot(<Comp $submit />)).toMatchInlineSnapshot(`
|
|
394
|
+
<button
|
|
395
|
+
type="submit"
|
|
396
|
+
/>
|
|
397
|
+
`);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("should have optional attrs props as component interface", () => {
|
|
401
|
+
const Comp = styled.h1.attrs<{ $primary?: boolean }>({
|
|
402
|
+
$primary: true,
|
|
403
|
+
})``;
|
|
404
|
+
|
|
405
|
+
expect(getSnapshot(<Comp />)).toMatchInlineSnapshot(`<h1 />`);
|
|
406
|
+
expect(getSnapshot(<Comp $primary />)).toMatchInlineSnapshot(`<h1 />`);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("should have access to theme", () => {
|
|
410
|
+
const ThemePrinter = ({ theme, ...props }: { theme?: unknown }) => (
|
|
411
|
+
<pre {...props}>{JSON.stringify(theme)}</pre>
|
|
412
|
+
);
|
|
413
|
+
const Comp = styled(ThemePrinter).attrs<DataAttributes>((p) => ({
|
|
414
|
+
"data-color": (p.theme as { color: string }).color,
|
|
415
|
+
}))``;
|
|
416
|
+
|
|
417
|
+
expect(
|
|
418
|
+
getSnapshot(
|
|
419
|
+
<YakThemeProvider theme={{ color: "red" }}>
|
|
420
|
+
<Comp />
|
|
421
|
+
</YakThemeProvider>,
|
|
422
|
+
),
|
|
423
|
+
).toMatchInlineSnapshot(`
|
|
424
|
+
<pre
|
|
425
|
+
data-color="red"
|
|
426
|
+
/>
|
|
427
|
+
`);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("should pass theme if theme is overwritten", () => {
|
|
431
|
+
const ThemePrinter = ({ theme, ...props }: { theme?: unknown }) => (
|
|
432
|
+
<pre {...props}>{JSON.stringify(theme)}</pre>
|
|
433
|
+
);
|
|
434
|
+
const Comp = styled(ThemePrinter).attrs({
|
|
435
|
+
theme: { color: "blue" },
|
|
436
|
+
})``;
|
|
437
|
+
|
|
438
|
+
expect(
|
|
439
|
+
getSnapshot(
|
|
440
|
+
<YakThemeProvider theme={{ color: "red" }}>
|
|
441
|
+
<Comp />
|
|
442
|
+
</YakThemeProvider>,
|
|
443
|
+
),
|
|
444
|
+
).toMatchInlineSnapshot(`
|
|
445
|
+
<pre>
|
|
446
|
+
{"color":"blue"}
|
|
447
|
+
</pre>
|
|
448
|
+
`);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
describe("attrs bug issue #163", () => {
|
|
452
|
+
it("should allow to delete a prop", () => {
|
|
453
|
+
const Comp = styled.h1.attrs<{ primary?: boolean }>({
|
|
454
|
+
primary: undefined,
|
|
455
|
+
})``;
|
|
456
|
+
|
|
457
|
+
expect(getSnapshot(<Comp primary />)).toMatchInlineSnapshot(`<h1 />`);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("should allow to rename a prop", () => {
|
|
461
|
+
const Comp = styled.h1.attrs<{ primary?: boolean }>((p) => ({
|
|
462
|
+
...p,
|
|
463
|
+
primary: undefined,
|
|
464
|
+
$primary: p.primary,
|
|
465
|
+
}))``;
|
|
466
|
+
|
|
467
|
+
expect(getSnapshot(<Comp primary />)).toMatchInlineSnapshot(`<h1 />`);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it("should allow to rename a prop in overwritten attrs", () => {
|
|
471
|
+
const Parent = styled.h1.attrs<{ count: number }>((p) => {
|
|
472
|
+
expect(p.count).toBe(1);
|
|
473
|
+
return {
|
|
474
|
+
count: ++p.count,
|
|
475
|
+
};
|
|
476
|
+
})``;
|
|
477
|
+
const Comp = styled(Parent).attrs<{ count: number }>((p) => {
|
|
478
|
+
expect(p.count).toBe(2);
|
|
479
|
+
return {
|
|
480
|
+
count: ++p.count,
|
|
481
|
+
};
|
|
482
|
+
})``;
|
|
483
|
+
|
|
484
|
+
expect(getSnapshot(<Comp count={1} />)).toMatchInlineSnapshot(`
|
|
485
|
+
<h1
|
|
486
|
+
count="3"
|
|
487
|
+
/>
|
|
488
|
+
`);
|
|
489
|
+
});
|
|
490
|
+
});
|