@vanilla-bean/components 1.0.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/Component/Component.js +598 -0
- package/Component/Component.scenarios.js +88 -0
- package/Component/Component.test.js +717 -0
- package/Component/README.md +455 -0
- package/Component/index.js +3 -0
- package/Component/observeElementConnection.js +52 -0
- package/Component/observeElementConnection.test.js +121 -0
- package/Elem/Elem.js +304 -0
- package/Elem/Elem.test.js +679 -0
- package/Elem/README.md +373 -0
- package/Elem/index.js +1 -0
- package/FontWithASyntaxHighlighter-Regular.woff2 +0 -0
- package/LICENSE +21 -0
- package/README.md +413 -0
- package/components/BottomSheet/BottomSheet.js +192 -0
- package/components/BottomSheet/BottomSheet.lld.md +25 -0
- package/components/BottomSheet/README.md +66 -0
- package/components/BottomSheet/index.js +1 -0
- package/components/Button/Button.js +53 -0
- package/components/Button/Button.lld.md +21 -0
- package/components/Button/index.js +1 -0
- package/components/Calendar/Calendar.js +720 -0
- package/components/Calendar/Calendar.lld.md +22 -0
- package/components/Calendar/CalendarEvent.js +102 -0
- package/components/Calendar/Toolbar.js +78 -0
- package/components/Calendar/index.js +2 -0
- package/components/Calendar/utils.js +56 -0
- package/components/Code/Code.js +84 -0
- package/components/Code/Code.lld.md +21 -0
- package/components/Code/index.js +1 -0
- package/components/ColorPicker/ColorPicker.js +445 -0
- package/components/ColorPicker/ColorPicker.lld.md +21 -0
- package/components/ColorPicker/index.js +1 -0
- package/components/ColorPicker/svg.js +5 -0
- package/components/Dialog/Dialog.js +278 -0
- package/components/Dialog/Dialog.lld.md +20 -0
- package/components/Dialog/README.md +96 -0
- package/components/Dialog/index.js +1 -0
- package/components/Form/Form.js +257 -0
- package/components/Form/Form.lld.md +21 -0
- package/components/Form/README.md +87 -0
- package/components/Form/index.js +1 -0
- package/components/Icon/Icon.js +54 -0
- package/components/Icon/Icon.lld.md +21 -0
- package/components/Icon/index.js +1 -0
- package/components/Input/Input.js +173 -0
- package/components/Input/Input.lld.md +28 -0
- package/components/Input/README.md +97 -0
- package/components/Input/index.js +2 -0
- package/components/Input/utils.js +122 -0
- package/components/Keyboard/Key.js +38 -0
- package/components/Keyboard/Keyboard.js +173 -0
- package/components/Keyboard/Keyboard.lld.md +21 -0
- package/components/Keyboard/index.js +1 -0
- package/components/Label/Label.js +214 -0
- package/components/Label/Label.lld.md +20 -0
- package/components/Label/index.js +1 -0
- package/components/Link/Link.js +43 -0
- package/components/Link/Link.lld.md +15 -0
- package/components/Link/index.js +1 -0
- package/components/List/List.js +82 -0
- package/components/List/List.lld.md +19 -0
- package/components/List/index.js +1 -0
- package/components/Menu/Menu.js +93 -0
- package/components/Menu/Menu.lld.md +15 -0
- package/components/Menu/index.js +1 -0
- package/components/Notify/Notify.js +96 -0
- package/components/Notify/Notify.lld.md +20 -0
- package/components/Notify/index.js +1 -0
- package/components/Page/Page.js +67 -0
- package/components/Page/Page.lld.md +20 -0
- package/components/Page/index.js +1 -0
- package/components/Popover/Popover.js +175 -0
- package/components/Popover/Popover.lld.md +19 -0
- package/components/Popover/index.js +1 -0
- package/components/RadioButton/RadioButton.js +108 -0
- package/components/RadioButton/RadioButton.lld.md +15 -0
- package/components/RadioButton/index.js +1 -0
- package/components/Router/README.md +160 -0
- package/components/Router/Router.js +150 -0
- package/components/Router/Router.lld.md +31 -0
- package/components/Router/View.js +15 -0
- package/components/Router/index.js +2 -0
- package/components/Router/utils.js +17 -0
- package/components/Select/README.md +88 -0
- package/components/Select/Select.js +74 -0
- package/components/Select/Select.lld.md +20 -0
- package/components/Select/index.js +1 -0
- package/components/Table/README.md +94 -0
- package/components/Table/Table.js +171 -0
- package/components/Table/Table.lld.md +21 -0
- package/components/Table/index.js +1 -0
- package/components/TagList/Tag.js +84 -0
- package/components/TagList/TagList.js +118 -0
- package/components/TagList/TagList.lld.md +30 -0
- package/components/TagList/design.excalidraw.png +0 -0
- package/components/TagList/index.js +2 -0
- package/components/Tooltip/Tooltip.js +139 -0
- package/components/Tooltip/Tooltip.lld.md +22 -0
- package/components/Tooltip/index.js +1 -0
- package/components/TooltipWrapper/TooltipWrapper.js +89 -0
- package/components/TooltipWrapper/TooltipWrapper.lld.md +21 -0
- package/components/TooltipWrapper/index.js +1 -0
- package/components/Whiteboard/Whiteboard.js +198 -0
- package/components/Whiteboard/Whiteboard.lld.md +35 -0
- package/components/Whiteboard/index.js +1 -0
- package/components/index.js +27 -0
- package/eslint.config.cjs +118 -0
- package/index.d.ts +635 -0
- package/index.js +19 -0
- package/package.json +123 -0
- package/plugins/asText.js +38 -0
- package/plugins/loadPlugins.js +5 -0
- package/plugins/markdownLoader.js +121 -0
- package/prettier.config.cjs +7 -0
- package/spellcheck.config.cjs +227 -0
- package/styled/README.md +329 -0
- package/styled/appendStyles.js +26 -0
- package/styled/appendStyles.test.js +45 -0
- package/styled/index.js +4 -0
- package/styled/shimCSS.js +31 -0
- package/styled/shimCSS.test.js +103 -0
- package/styled/styled.js +91 -0
- package/styled/styled.test.js +586 -0
- package/styled/themeStyles.js +36 -0
- package/styled/themeStyles.test.js +135 -0
- package/test-setup.js +123 -0
- package/theme/.test.js +69 -0
- package/theme/README.md +607 -0
- package/theme/button.js +100 -0
- package/theme/code.js +123 -0
- package/theme/colors.js +42 -0
- package/theme/fonts.js +42 -0
- package/theme/index.js +33 -0
- package/theme/input.js +64 -0
- package/theme/page.js +208 -0
- package/theme/scrollbar.js +24 -0
- package/theme/table.js +53 -0
- package/utils/README.md +176 -0
- package/utils/browser.js +92 -0
- package/utils/class.js +30 -0
- package/utils/color.js +81 -0
- package/utils/data.js +164 -0
- package/utils/element.js +55 -0
- package/utils/index.js +7 -0
- package/utils/rand.js +12 -0
- package/utils/string.js +72 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
|
|
3
|
+
import { Component } from '../Component';
|
|
4
|
+
import theme from '../theme';
|
|
5
|
+
import { styled, configured } from './styled';
|
|
6
|
+
|
|
7
|
+
const mockShimCSS = mock(() => {});
|
|
8
|
+
mock.module('./shimCSS', () => ({ shimCSS: mockShimCSS }));
|
|
9
|
+
|
|
10
|
+
const mockNanoid = mock(() => 'test-id-1234567890');
|
|
11
|
+
mock.module('../utils', () => ({ classSafeNanoid: mockNanoid }));
|
|
12
|
+
|
|
13
|
+
describe('styled system', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
mockShimCSS.mockClear();
|
|
16
|
+
mockNanoid.mockClear();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('configured()', () => {
|
|
20
|
+
test('creates component with embedded options', () => {
|
|
21
|
+
const options = { tag: 'article', textContent: 'default', customProp: 'test' };
|
|
22
|
+
const ConfiguredComponent = configured(Component, options);
|
|
23
|
+
const instance = new ConfiguredComponent({ textContent: 'override' });
|
|
24
|
+
|
|
25
|
+
expect(instance.elem.tagName.toLowerCase()).toBe('article');
|
|
26
|
+
expect(instance.elem.textContent).toBe('override');
|
|
27
|
+
expect(instance.options.customProp).toBe('test');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('merges addClass from both options and instance', () => {
|
|
31
|
+
const ConfiguredComponent = configured(Component, { addClass: 'base-class' });
|
|
32
|
+
const instance = new ConfiguredComponent({ addClass: 'instance-class' });
|
|
33
|
+
|
|
34
|
+
expect(instance.elem.classList.contains('base-class')).toBe(true);
|
|
35
|
+
expect(instance.elem.classList.contains('instance-class')).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('overlays instance options over base options', () => {
|
|
39
|
+
const baseOptions = {
|
|
40
|
+
tag: 'section',
|
|
41
|
+
role: 'banner',
|
|
42
|
+
textContent: 'base',
|
|
43
|
+
customValue: 'base',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const ConfiguredComponent = configured(Component, baseOptions);
|
|
47
|
+
const instance = new ConfiguredComponent({
|
|
48
|
+
textContent: 'override',
|
|
49
|
+
customValue: 'override',
|
|
50
|
+
newProp: 'new',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(instance.elem.tagName.toLowerCase()).toBe('section');
|
|
54
|
+
expect(instance.elem.getAttribute('role')).toBe('banner');
|
|
55
|
+
expect(instance.elem.textContent).toBe('override');
|
|
56
|
+
expect(instance.options.customValue).toBe('override');
|
|
57
|
+
expect(instance.options.newProp).toBe('new');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('handles children correctly', () => {
|
|
61
|
+
const ConfiguredComponent = configured(Component, { tag: 'div' });
|
|
62
|
+
const child1 = new Component({ textContent: 'child1' });
|
|
63
|
+
const child2 = new Component({ textContent: 'child2' });
|
|
64
|
+
|
|
65
|
+
const instance = new ConfiguredComponent({}, child1, child2);
|
|
66
|
+
|
|
67
|
+
expect(instance.elem.children).toHaveLength(2);
|
|
68
|
+
expect(instance.elem.children[0].textContent).toBe('child1');
|
|
69
|
+
expect(instance.elem.children[1].textContent).toBe('child2');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('styled() function syntax', () => {
|
|
74
|
+
test('creates component with scoped styles', () => {
|
|
75
|
+
const testCSS = 'color: red; display: flex;';
|
|
76
|
+
const StyledComponent = styled(Component, () => testCSS);
|
|
77
|
+
|
|
78
|
+
expect(mockShimCSS).toHaveBeenCalledTimes(1);
|
|
79
|
+
const shimCall = mockShimCSS.mock.calls[0][0];
|
|
80
|
+
expect(shimCall).toHaveProperty('styles');
|
|
81
|
+
expect(shimCall).toHaveProperty('scope');
|
|
82
|
+
expect(shimCall.scope).toBe('.test-id-1234567890');
|
|
83
|
+
|
|
84
|
+
const instance = new StyledComponent({ textContent: 'test' });
|
|
85
|
+
expect(instance.elem.textContent).toBe('test');
|
|
86
|
+
|
|
87
|
+
expect(instance.elem.classList.contains('test-id-1234567890')).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('applies theme to styles function', () => {
|
|
91
|
+
const stylesFunction = mock(({ colors }) => `color: ${colors.red};`);
|
|
92
|
+
styled(Component, stylesFunction);
|
|
93
|
+
|
|
94
|
+
expect(mockShimCSS).toHaveBeenCalledTimes(1);
|
|
95
|
+
const shimCall = mockShimCSS.mock.calls[0][0];
|
|
96
|
+
|
|
97
|
+
const result = shimCall.styles(theme);
|
|
98
|
+
expect(stylesFunction).toHaveBeenCalledWith(theme);
|
|
99
|
+
expect(result).toContain(theme.colors.red.toString());
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('merges configuration options with styles', () => {
|
|
103
|
+
const options = { tag: 'section', role: 'banner', testProp: 'value' };
|
|
104
|
+
const StyledComponent = styled(Component, () => 'color: blue;', options);
|
|
105
|
+
const instance = new StyledComponent({ textContent: 'test' });
|
|
106
|
+
|
|
107
|
+
expect(instance.elem.tagName.toLowerCase()).toBe('section');
|
|
108
|
+
expect(instance.elem.getAttribute('role')).toBe('banner');
|
|
109
|
+
expect(instance.options.testProp).toBe('value');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('creates unique scoped class identifiers for each component', () => {
|
|
113
|
+
mockNanoid.mockReturnValueOnce('unique-id-1').mockReturnValueOnce('unique-id-2');
|
|
114
|
+
|
|
115
|
+
styled(Component, () => 'color: red;');
|
|
116
|
+
styled(Component, () => 'color: blue;');
|
|
117
|
+
|
|
118
|
+
expect(mockShimCSS).toHaveBeenCalledTimes(2);
|
|
119
|
+
|
|
120
|
+
const scope1 = mockShimCSS.mock.calls[0][0].scope;
|
|
121
|
+
const scope2 = mockShimCSS.mock.calls[1][0].scope;
|
|
122
|
+
|
|
123
|
+
expect(scope1).toBe('.unique-id-1');
|
|
124
|
+
expect(scope2).toBe('.unique-id-2');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('handles empty styles function', () => {
|
|
128
|
+
styled(Component);
|
|
129
|
+
|
|
130
|
+
expect(mockShimCSS).not.toHaveBeenCalled();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('combines addClass from options with generated scope class', () => {
|
|
134
|
+
const StyledComponent = styled(Component, () => 'color: red;', { addClass: 'custom-class' });
|
|
135
|
+
const instance = new StyledComponent();
|
|
136
|
+
|
|
137
|
+
expect(instance.elem.classList.contains('test-id-1234567890')).toBe(true);
|
|
138
|
+
expect(instance.elem.classList.contains('custom-class')).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('styled() template literal syntax', () => {
|
|
143
|
+
test('detects template literal syntax', () => {
|
|
144
|
+
mockShimCSS.mockClear();
|
|
145
|
+
|
|
146
|
+
const mockTemplateLiteral = ['color: red; background: blue;'];
|
|
147
|
+
mockTemplateLiteral.raw = ['color: red; background: blue;'];
|
|
148
|
+
|
|
149
|
+
styled(Component, mockTemplateLiteral);
|
|
150
|
+
|
|
151
|
+
expect(mockShimCSS).toHaveBeenCalledTimes(1);
|
|
152
|
+
const shimCall = mockShimCSS.mock.calls[0][0];
|
|
153
|
+
expect(shimCall.styles).toBeInstanceOf(Function);
|
|
154
|
+
|
|
155
|
+
const result = shimCall.styles();
|
|
156
|
+
expect(result).toContain('color: red');
|
|
157
|
+
expect(result).toContain('background: blue');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('interpolates functions in template literals', () => {
|
|
161
|
+
mockShimCSS.mockClear();
|
|
162
|
+
|
|
163
|
+
const mockTemplateLiteral = ['color: ', '; background: ', ';'];
|
|
164
|
+
mockTemplateLiteral.raw = ['color: ', '; background: ', ';'];
|
|
165
|
+
|
|
166
|
+
const colorFunction = ({ colors }) => colors.red;
|
|
167
|
+
const bgFunction = ({ colors }) => colors.blue;
|
|
168
|
+
|
|
169
|
+
styled(Component, mockTemplateLiteral, colorFunction, bgFunction);
|
|
170
|
+
|
|
171
|
+
expect(mockShimCSS).toHaveBeenCalledTimes(1);
|
|
172
|
+
const shimCall = mockShimCSS.mock.calls[0][0];
|
|
173
|
+
|
|
174
|
+
const result = shimCall.styles();
|
|
175
|
+
expect(result).toContain(`color: ${theme.colors.red}`);
|
|
176
|
+
expect(result).toContain(`background: ${theme.colors.blue}`);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('interpolates static values in template literals', () => {
|
|
180
|
+
mockShimCSS.mockClear();
|
|
181
|
+
|
|
182
|
+
const mockTemplateLiteral = ['margin: ', '; color: red;'];
|
|
183
|
+
mockTemplateLiteral.raw = ['margin: ', '; color: red;'];
|
|
184
|
+
|
|
185
|
+
const staticValue = '15px';
|
|
186
|
+
styled(Component, mockTemplateLiteral, staticValue);
|
|
187
|
+
|
|
188
|
+
const shimCall = mockShimCSS.mock.calls[0][0];
|
|
189
|
+
const result = shimCall.styles();
|
|
190
|
+
|
|
191
|
+
expect(result).toContain(`margin: ${staticValue}`);
|
|
192
|
+
expect(result).toContain('color: red');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('template literal creates component with empty options', () => {
|
|
196
|
+
mockShimCSS.mockClear();
|
|
197
|
+
|
|
198
|
+
const mockTemplateLiteral = ['color: red;'];
|
|
199
|
+
mockTemplateLiteral.raw = ['color: red;'];
|
|
200
|
+
|
|
201
|
+
const StyledComponent = styled(Component, mockTemplateLiteral);
|
|
202
|
+
const instance = new StyledComponent();
|
|
203
|
+
|
|
204
|
+
expect(instance.elem.tagName.toLowerCase()).toBe('div');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('handles empty template literal', () => {
|
|
208
|
+
mockShimCSS.mockClear();
|
|
209
|
+
|
|
210
|
+
const mockTemplateLiteral = [''];
|
|
211
|
+
mockTemplateLiteral.raw = [''];
|
|
212
|
+
|
|
213
|
+
styled(Component, mockTemplateLiteral);
|
|
214
|
+
|
|
215
|
+
const shimCall = mockShimCSS.mock.calls[0][0];
|
|
216
|
+
const result = shimCall.styles();
|
|
217
|
+
|
|
218
|
+
expect(result).toBe('');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('processes multi-line template literal', () => {
|
|
222
|
+
mockShimCSS.mockClear();
|
|
223
|
+
|
|
224
|
+
const mockTemplateLiteral = [
|
|
225
|
+
`
|
|
226
|
+
display: flex;
|
|
227
|
+
flex-direction: column;
|
|
228
|
+
|
|
229
|
+
.child {
|
|
230
|
+
margin: 5px;
|
|
231
|
+
}
|
|
232
|
+
`,
|
|
233
|
+
];
|
|
234
|
+
mockTemplateLiteral.raw = [
|
|
235
|
+
`
|
|
236
|
+
display: flex;
|
|
237
|
+
flex-direction: column;
|
|
238
|
+
|
|
239
|
+
.child {
|
|
240
|
+
margin: 5px;
|
|
241
|
+
}
|
|
242
|
+
`,
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
styled(Component, mockTemplateLiteral);
|
|
246
|
+
|
|
247
|
+
const shimCall = mockShimCSS.mock.calls[0][0];
|
|
248
|
+
const result = shimCall.styles();
|
|
249
|
+
|
|
250
|
+
expect(result).toContain('display: flex');
|
|
251
|
+
expect(result).toContain('flex-direction: column');
|
|
252
|
+
expect(result).toContain('.child');
|
|
253
|
+
expect(result).toContain('margin: 5px');
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('component inheritance and behavior', () => {
|
|
258
|
+
test('styled component inherits all base component functionality', () => {
|
|
259
|
+
const StyledComponent = styled(Component, () => 'color: red;');
|
|
260
|
+
const instance = new StyledComponent({
|
|
261
|
+
textContent: 'test content',
|
|
262
|
+
onClick: () => 'clicked',
|
|
263
|
+
addClass: 'additional-class',
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(instance.elem.textContent).toBe('test content');
|
|
267
|
+
expect(instance.elem.classList.contains('additional-class')).toBe(true);
|
|
268
|
+
expect(typeof instance.on).toBe('function');
|
|
269
|
+
expect(typeof instance.render).toBe('function');
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('nested styled components', () => {
|
|
274
|
+
test('creates styled component from another styled component', () => {
|
|
275
|
+
mockNanoid.mockReturnValueOnce('base-id').mockReturnValueOnce('nested-id');
|
|
276
|
+
|
|
277
|
+
const BaseStyledComponent = styled(Component, () => 'color: red; margin: 10px;');
|
|
278
|
+
const NestedStyledComponent = styled(BaseStyledComponent, () => 'background: blue; padding: 5px;');
|
|
279
|
+
|
|
280
|
+
expect(mockShimCSS).toHaveBeenCalledTimes(2);
|
|
281
|
+
|
|
282
|
+
const instance = new NestedStyledComponent({ textContent: 'nested test' });
|
|
283
|
+
|
|
284
|
+
expect(instance.elem.textContent).toBe('nested test');
|
|
285
|
+
expect(instance.elem.classList.contains('base-id')).toBe(true);
|
|
286
|
+
expect(instance.elem.classList.contains('nested-id')).toBe(true);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('nested styled component inherits base options', () => {
|
|
290
|
+
mockNanoid.mockReturnValueOnce('base-id').mockReturnValueOnce('nested-id');
|
|
291
|
+
|
|
292
|
+
const BaseStyledComponent = styled(Component, () => 'color: red;', {
|
|
293
|
+
tag: 'section',
|
|
294
|
+
role: 'banner',
|
|
295
|
+
baseProp: 'base-value',
|
|
296
|
+
});
|
|
297
|
+
const NestedStyledComponent = styled(BaseStyledComponent, () => 'background: blue;', {
|
|
298
|
+
nestedProp: 'nested-value',
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const instance = new NestedStyledComponent({ textContent: 'test' });
|
|
302
|
+
|
|
303
|
+
expect(instance.elem.tagName.toLowerCase()).toBe('section');
|
|
304
|
+
expect(instance.elem.getAttribute('role')).toBe('banner');
|
|
305
|
+
expect(instance.options.baseProp).toBe('base-value');
|
|
306
|
+
expect(instance.options.nestedProp).toBe('nested-value');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('nested styled component merges addClass correctly', () => {
|
|
310
|
+
mockNanoid.mockReturnValueOnce('base-id').mockReturnValueOnce('nested-id');
|
|
311
|
+
|
|
312
|
+
const BaseStyledComponent = styled(Component, () => 'color: red;', {
|
|
313
|
+
addClass: 'base-class',
|
|
314
|
+
});
|
|
315
|
+
const NestedStyledComponent = styled(BaseStyledComponent, () => 'background: blue;', {
|
|
316
|
+
addClass: 'nested-class',
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const instance = new NestedStyledComponent({ addClass: 'instance-class' });
|
|
320
|
+
|
|
321
|
+
expect(instance.elem.classList.contains('base-id')).toBe(true);
|
|
322
|
+
expect(instance.elem.classList.contains('nested-id')).toBe(true);
|
|
323
|
+
expect(instance.elem.classList.contains('base-class')).toBe(true);
|
|
324
|
+
expect(instance.elem.classList.contains('nested-class')).toBe(true);
|
|
325
|
+
expect(instance.elem.classList.contains('instance-class')).toBe(true);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('styles are processed independently for each level', () => {
|
|
329
|
+
mockNanoid.mockReturnValueOnce('base-id').mockReturnValueOnce('nested-id');
|
|
330
|
+
|
|
331
|
+
const baseStyles = ({ colors }) => `color: ${colors.red};`;
|
|
332
|
+
const nestedStyles = ({ colors }) => `background: ${colors.blue};`;
|
|
333
|
+
|
|
334
|
+
const BaseStyledComponent = styled(Component, baseStyles);
|
|
335
|
+
styled(BaseStyledComponent, nestedStyles);
|
|
336
|
+
|
|
337
|
+
expect(mockShimCSS).toHaveBeenCalledTimes(2);
|
|
338
|
+
|
|
339
|
+
const baseShimCall = mockShimCSS.mock.calls[0][0];
|
|
340
|
+
const nestedShimCall = mockShimCSS.mock.calls[1][0];
|
|
341
|
+
|
|
342
|
+
expect(baseShimCall.scope).toBe('.base-id');
|
|
343
|
+
expect(nestedShimCall.scope).toBe('.nested-id');
|
|
344
|
+
|
|
345
|
+
const baseResult = baseShimCall.styles(theme);
|
|
346
|
+
const nestedResult = nestedShimCall.styles(theme);
|
|
347
|
+
|
|
348
|
+
expect(baseResult).toContain(theme.colors.red.toString());
|
|
349
|
+
expect(nestedResult).toContain(theme.colors.blue.toString());
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('handles multiple levels of nesting (3 levels deep)', () => {
|
|
353
|
+
mockNanoid.mockReturnValueOnce('level1-id').mockReturnValueOnce('level2-id').mockReturnValueOnce('level3-id');
|
|
354
|
+
|
|
355
|
+
const Level1Component = styled(Component, () => 'color: red;', {
|
|
356
|
+
tag: 'article',
|
|
357
|
+
level1Prop: 'value1',
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const Level2Component = styled(Level1Component, () => 'background: blue;', {
|
|
361
|
+
level2Prop: 'value2',
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const Level3Component = styled(Level2Component, () => 'border: 1px solid black;', {
|
|
365
|
+
level3Prop: 'value3',
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
expect(mockShimCSS).toHaveBeenCalledTimes(3);
|
|
369
|
+
|
|
370
|
+
const instance = new Level3Component({ textContent: 'deep nested' });
|
|
371
|
+
|
|
372
|
+
expect(instance.elem.textContent).toBe('deep nested');
|
|
373
|
+
expect(instance.elem.tagName.toLowerCase()).toBe('article');
|
|
374
|
+
|
|
375
|
+
expect(instance.elem.classList.contains('level1-id')).toBe(true);
|
|
376
|
+
expect(instance.elem.classList.contains('level2-id')).toBe(true);
|
|
377
|
+
expect(instance.elem.classList.contains('level3-id')).toBe(true);
|
|
378
|
+
|
|
379
|
+
expect(instance.options.level1Prop).toBe('value1');
|
|
380
|
+
expect(instance.options.level2Prop).toBe('value2');
|
|
381
|
+
expect(instance.options.level3Prop).toBe('value3');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test('nested styled components work with template literal syntax', () => {
|
|
385
|
+
mockNanoid.mockReturnValueOnce('base-id').mockReturnValueOnce('nested-id');
|
|
386
|
+
|
|
387
|
+
const mockBaseLiteral = ['color: red; font-size: 16px;'];
|
|
388
|
+
mockBaseLiteral.raw = ['color: red; font-size: 16px;'];
|
|
389
|
+
|
|
390
|
+
const mockNestedLiteral = ['background: blue; padding: ', ';'];
|
|
391
|
+
mockNestedLiteral.raw = ['background: blue; padding: ', ';'];
|
|
392
|
+
|
|
393
|
+
const BaseStyledComponent = styled(Component, mockBaseLiteral);
|
|
394
|
+
const NestedStyledComponent = styled(BaseStyledComponent, mockNestedLiteral, '10px');
|
|
395
|
+
|
|
396
|
+
expect(mockShimCSS).toHaveBeenCalledTimes(2);
|
|
397
|
+
|
|
398
|
+
const instance = new NestedStyledComponent();
|
|
399
|
+
|
|
400
|
+
expect(instance.elem.classList.contains('base-id')).toBe(true);
|
|
401
|
+
expect(instance.elem.classList.contains('nested-id')).toBe(true);
|
|
402
|
+
|
|
403
|
+
const baseShimCall = mockShimCSS.mock.calls[0][0];
|
|
404
|
+
const nestedShimCall = mockShimCSS.mock.calls[1][0];
|
|
405
|
+
|
|
406
|
+
expect(baseShimCall.styles()).toContain('color: red');
|
|
407
|
+
expect(nestedShimCall.styles()).toContain('padding: 10px');
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe('CSS specificity and class order', () => {
|
|
412
|
+
test('nested styled components maintain correct class order for specificity', () => {
|
|
413
|
+
mockNanoid.mockReturnValueOnce('base-scope').mockReturnValueOnce('nested-scope');
|
|
414
|
+
|
|
415
|
+
const BaseComponent = styled(Component, () => 'color: red;', { addClass: 'base-class' });
|
|
416
|
+
const NestedComponent = styled(BaseComponent, () => 'color: blue;', { addClass: 'nested-class' });
|
|
417
|
+
|
|
418
|
+
const instance = new NestedComponent({ addClass: 'instance-class' });
|
|
419
|
+
|
|
420
|
+
const classList = Array.from(instance.elem.classList);
|
|
421
|
+
|
|
422
|
+
expect(classList).toContain('base-scope');
|
|
423
|
+
expect(classList).toContain('nested-scope');
|
|
424
|
+
expect(classList).toContain('base-class');
|
|
425
|
+
expect(classList).toContain('nested-class');
|
|
426
|
+
expect(classList).toContain('instance-class');
|
|
427
|
+
|
|
428
|
+
const baseScopeIndex = classList.indexOf('base-scope');
|
|
429
|
+
const nestedScopeIndex = classList.indexOf('nested-scope');
|
|
430
|
+
|
|
431
|
+
expect(baseScopeIndex).toBeLessThan(nestedScopeIndex);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test('each nested level gets unique CSS scope and independent style processing', () => {
|
|
435
|
+
mockNanoid.mockReturnValueOnce('scope-1').mockReturnValueOnce('scope-2').mockReturnValueOnce('scope-3');
|
|
436
|
+
|
|
437
|
+
const Component1 = styled(Component, ({ colors }) => `color: ${colors.red}; font-size: 12px;`);
|
|
438
|
+
const Component2 = styled(Component1, ({ colors }) => `background: ${colors.blue}; margin: 5px;`);
|
|
439
|
+
styled(Component2, ({ colors }) => `border: 1px solid ${colors.green}; padding: 10px;`);
|
|
440
|
+
|
|
441
|
+
expect(mockShimCSS).toHaveBeenCalledTimes(3);
|
|
442
|
+
|
|
443
|
+
const scope1Call = mockShimCSS.mock.calls[0][0];
|
|
444
|
+
const scope2Call = mockShimCSS.mock.calls[1][0];
|
|
445
|
+
const scope3Call = mockShimCSS.mock.calls[2][0];
|
|
446
|
+
|
|
447
|
+
expect(scope1Call.scope).toBe('.scope-1');
|
|
448
|
+
expect(scope2Call.scope).toBe('.scope-2');
|
|
449
|
+
expect(scope3Call.scope).toBe('.scope-3');
|
|
450
|
+
|
|
451
|
+
const styles1 = scope1Call.styles(theme);
|
|
452
|
+
const styles2 = scope2Call.styles(theme);
|
|
453
|
+
const styles3 = scope3Call.styles(theme);
|
|
454
|
+
|
|
455
|
+
expect(styles1).toContain(`color: ${theme.colors.red}`);
|
|
456
|
+
expect(styles1).toContain('font-size: 12px');
|
|
457
|
+
|
|
458
|
+
expect(styles2).toContain(`background: ${theme.colors.blue}`);
|
|
459
|
+
expect(styles2).toContain('margin: 5px');
|
|
460
|
+
|
|
461
|
+
expect(styles3).toContain(`border: 1px solid ${theme.colors.green}`);
|
|
462
|
+
expect(styles3).toContain('padding: 10px');
|
|
463
|
+
|
|
464
|
+
expect(styles1).not.toContain('background');
|
|
465
|
+
expect(styles1).not.toContain('border');
|
|
466
|
+
expect(styles2).not.toContain('border');
|
|
467
|
+
expect(styles2).not.toContain('color');
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test('configured options merge correctly across nested levels', () => {
|
|
471
|
+
const Level1 = styled(Component, () => 'color: red;', {
|
|
472
|
+
tag: 'section',
|
|
473
|
+
role: 'banner',
|
|
474
|
+
prop1: 'value1',
|
|
475
|
+
addClass: 'class1',
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const Level2 = styled(Level1, () => 'background: blue;', {
|
|
479
|
+
role: 'main',
|
|
480
|
+
prop2: 'value2',
|
|
481
|
+
addClass: 'class2',
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
const Level3 = styled(Level2, () => 'border: solid;', {
|
|
485
|
+
prop3: 'value3',
|
|
486
|
+
addClass: 'class3',
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
const instance = new Level3({
|
|
490
|
+
prop1: 'override1',
|
|
491
|
+
instanceProp: 'instanceValue',
|
|
492
|
+
addClass: 'instanceClass',
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
expect(instance.elem.tagName.toLowerCase()).toBe('section');
|
|
496
|
+
expect(instance.elem.getAttribute('role')).toBe('main');
|
|
497
|
+
|
|
498
|
+
expect(instance.options.prop1).toBe('override1');
|
|
499
|
+
expect(instance.options.prop2).toBe('value2');
|
|
500
|
+
expect(instance.options.prop3).toBe('value3');
|
|
501
|
+
expect(instance.options.instanceProp).toBe('instanceValue');
|
|
502
|
+
|
|
503
|
+
expect(instance.elem.classList.contains('class1')).toBe(true);
|
|
504
|
+
expect(instance.elem.classList.contains('class2')).toBe(true);
|
|
505
|
+
expect(instance.elem.classList.contains('class3')).toBe(true);
|
|
506
|
+
expect(instance.elem.classList.contains('instanceClass')).toBe(true);
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
describe('constructor invocation errors', () => {
|
|
511
|
+
test('styled component must be called with new keyword', () => {
|
|
512
|
+
const StyledComponent = styled(Component, () => 'color: red;');
|
|
513
|
+
|
|
514
|
+
expect(() => {
|
|
515
|
+
StyledComponent();
|
|
516
|
+
}).toThrow(/Cannot call a class constructor|Class constructor.*cannot be invoked without/);
|
|
517
|
+
|
|
518
|
+
expect(() => {
|
|
519
|
+
new StyledComponent();
|
|
520
|
+
}).not.toThrow();
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test('nested styled component must be called with new keyword', () => {
|
|
524
|
+
const BaseStyledComponent = styled(Component, () => 'color: red;');
|
|
525
|
+
const NestedStyledComponent = styled(BaseStyledComponent, () => 'background: blue;');
|
|
526
|
+
|
|
527
|
+
expect(() => {
|
|
528
|
+
NestedStyledComponent();
|
|
529
|
+
}).toThrow(/Cannot call a class constructor|Class constructor.*cannot be invoked without/);
|
|
530
|
+
|
|
531
|
+
expect(() => {
|
|
532
|
+
new NestedStyledComponent();
|
|
533
|
+
}).not.toThrow();
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test('configured function works with already configured components', () => {
|
|
537
|
+
const BaseStyledComponent = styled(Component, () => 'color: red;');
|
|
538
|
+
|
|
539
|
+
expect(() => {
|
|
540
|
+
const DoubleConfigured = configured(BaseStyledComponent, { addClass: 'double' });
|
|
541
|
+
const instance = new DoubleConfigured();
|
|
542
|
+
|
|
543
|
+
expect(instance).toBeDefined();
|
|
544
|
+
}).not.toThrow();
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test('styled template literal with styled component returns valid class', () => {
|
|
548
|
+
const BaseStyledComponent = styled(Component, () => 'color: red;');
|
|
549
|
+
|
|
550
|
+
const result = styled(BaseStyledComponent)`
|
|
551
|
+
background: blue;
|
|
552
|
+
padding: 10px;
|
|
553
|
+
`;
|
|
554
|
+
|
|
555
|
+
expect(typeof result).toBe('function');
|
|
556
|
+
expect(result.prototype).toBeDefined();
|
|
557
|
+
expect(() => new result()).not.toThrow();
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test('class extends styled template literal should work correctly', () => {
|
|
561
|
+
const BaseStyledComponent = styled(Component, () => 'color: red;');
|
|
562
|
+
|
|
563
|
+
expect(() => {
|
|
564
|
+
// eslint-disable-next-line no-unused-vars
|
|
565
|
+
class ExtendedComponent extends (styled(BaseStyledComponent)`
|
|
566
|
+
background: blue;
|
|
567
|
+
padding: 10px;
|
|
568
|
+
`) {}
|
|
569
|
+
}).not.toThrow();
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test('class extends styled template literal can be instantiated', () => {
|
|
573
|
+
const BaseStyledComponent = styled(Component, () => 'color: red;');
|
|
574
|
+
|
|
575
|
+
expect(() => {
|
|
576
|
+
class ExtendedComponent extends (styled(BaseStyledComponent)`
|
|
577
|
+
background: blue;
|
|
578
|
+
`) {}
|
|
579
|
+
|
|
580
|
+
const instance = new ExtendedComponent();
|
|
581
|
+
|
|
582
|
+
expect(instance).toBeDefined();
|
|
583
|
+
}).not.toThrow();
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import theme from '../theme';
|
|
2
|
+
|
|
3
|
+
import { isDev } from '../utils/browser';
|
|
4
|
+
import { removeExcessIndentation } from '../utils/string';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} StyleConfig
|
|
8
|
+
* @property {Function} styles - Theme function that returns CSS string or style object
|
|
9
|
+
* @property {string} [scope] - Optional CSS selector to wrap the styles
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Process a theme function into CSS string with optional scoping
|
|
14
|
+
* Handles both CSS string and style object returns from theme functions
|
|
15
|
+
* @param {StyleConfig} styleConfig - Configuration with styles function and optional scope
|
|
16
|
+
* @returns {string|object} Processed CSS string (scoped if specified) or style object for direct application
|
|
17
|
+
*/
|
|
18
|
+
export const themeStyles = ({ styles = () => '', scope }) => {
|
|
19
|
+
const themedStyles = styles(theme);
|
|
20
|
+
|
|
21
|
+
if (typeof themedStyles === 'object') return themedStyles;
|
|
22
|
+
if (!/\S/.test(themedStyles)) return '';
|
|
23
|
+
|
|
24
|
+
let css = themedStyles;
|
|
25
|
+
|
|
26
|
+
if (scope) {
|
|
27
|
+
const keyframes = [];
|
|
28
|
+
const inner = themedStyles.replace(/@keyframes\s+[\w-]+\s*\{(?:[^{}]|\{[^{}]*\})*\}/g, match => {
|
|
29
|
+
keyframes.push(match);
|
|
30
|
+
return '';
|
|
31
|
+
});
|
|
32
|
+
css = keyframes.length ? `${keyframes.join('\n')}\n${scope} { ${inner} }` : `${scope} { ${inner} }`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (isDev ? x => x : removeExcessIndentation)(css);
|
|
36
|
+
};
|