@autoguru/overdrive 4.45.2 → 4.46.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/dist/components/DateTimePicker/CalendarButton.js +2 -2
- package/dist/components/DateTimePicker/CalendarGrid.js +2 -2
- package/dist/components/DateTimePicker/DateTimePicker.css.d.ts +2 -10
- package/dist/components/DateTimePicker/DateTimePicker.css.d.ts.map +1 -1
- package/dist/components/DateTimePicker/DateTimePicker.css.js +108 -81
- package/dist/components/OptionGrid/OptionGrid.css.d.ts +33 -34
- package/dist/components/OptionGrid/OptionGrid.css.d.ts.map +1 -1
- package/dist/components/OptionGrid/OptionGrid.css.js +208 -146
- package/dist/components/OptionGrid/OptionGrid.d.ts +16 -5
- package/dist/components/OptionGrid/OptionGrid.d.ts.map +1 -1
- package/dist/components/OptionGrid/OptionGrid.js +38 -13
- package/dist/components/OptionList/OptionList.css.d.ts +2 -10
- package/dist/components/OptionList/OptionList.css.d.ts.map +1 -1
- package/dist/components/OptionList/OptionList.css.js +92 -92
- package/dist/components/OptionList/OptionList.d.ts +7 -0
- package/dist/components/OptionList/OptionList.d.ts.map +1 -1
- package/dist/components/OptionList/OptionListItem.js +3 -3
- package/dist/components/SearchBar/SearchBar.css.d.ts +7 -12
- package/dist/components/SearchBar/SearchBar.css.d.ts.map +1 -1
- package/dist/components/SearchBar/SearchBar.css.js +121 -66
- package/dist/components/SearchBar/SearchBar.d.ts.map +1 -1
- package/dist/components/SearchBar/SearchBar.js +17 -10
- package/dist/hooks/useMedia/useMedia.spec.d.ts +2 -0
- package/dist/hooks/useMedia/useMedia.spec.d.ts.map +1 -0
- package/dist/hooks/useMedia/useMedia.spec.js +288 -0
- package/dist/styles/selectors.d.ts +13 -0
- package/dist/styles/selectors.d.ts.map +1 -0
- package/dist/styles/selectors.js +26 -0
- package/dist/utils/css.d.ts +0 -22
- package/dist/utils/css.d.ts.map +1 -1
- package/dist/utils/css.js +0 -52
- package/dist/utils/css.spec.d.ts +2 -0
- package/dist/utils/css.spec.d.ts.map +1 -0
- package/dist/utils/css.spec.js +66 -0
- package/dist/utils/object.spec.d.ts +2 -0
- package/dist/utils/object.spec.d.ts.map +1 -0
- package/dist/utils/object.spec.js +135 -0
- package/dist/utils/responsiveStyle.spec.d.ts +2 -0
- package/dist/utils/responsiveStyle.spec.d.ts.map +1 -0
- package/dist/utils/responsiveStyle.spec.js +134 -0
- package/dist/utils/utils.spec.d.ts.map +1 -1
- package/dist/utils/utils.spec.js +342 -0
- package/package.json +8 -8
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { renderHook, act } from '@testing-library/react';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { OverdriveProvider } from "../../components/OverdriveProvider/index.js";
|
|
6
|
+
import baseTheme from "../../themes/base/index.js";
|
|
7
|
+
import { useMedia } from "./useMedia.js"; // Mock the utils module to control isBrowser
|
|
8
|
+
vi.mock('../../utils', () => ({
|
|
9
|
+
isBrowser: true
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
// Mock window.matchMedia since it's not available in test environment
|
|
13
|
+
const mockMatchMedia = vi.fn();
|
|
14
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
15
|
+
writable: true,
|
|
16
|
+
value: mockMatchMedia
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Helper to create a mock MediaQueryList
|
|
20
|
+
const createMockMediaQueryList = (matches = false) => ({
|
|
21
|
+
matches,
|
|
22
|
+
media: '',
|
|
23
|
+
onchange: null,
|
|
24
|
+
addListener: vi.fn(),
|
|
25
|
+
// deprecated but still used by some
|
|
26
|
+
removeListener: vi.fn(),
|
|
27
|
+
// deprecated but still used by some
|
|
28
|
+
addEventListener: vi.fn(),
|
|
29
|
+
removeEventListener: vi.fn(),
|
|
30
|
+
dispatchEvent: vi.fn()
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Wrapper component with OverdriveProvider
|
|
34
|
+
const createWrapper = () => {
|
|
35
|
+
// eslint-disable-next-line react/display-name
|
|
36
|
+
return ({
|
|
37
|
+
children
|
|
38
|
+
}) => /*#__PURE__*/React.createElement(OverdriveProvider, {
|
|
39
|
+
theme: baseTheme
|
|
40
|
+
}, children);
|
|
41
|
+
};
|
|
42
|
+
describe('useMedia hook', () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
vi.clearAllMocks();
|
|
45
|
+
|
|
46
|
+
// Default mock implementation
|
|
47
|
+
mockMatchMedia.mockImplementation(() => createMockMediaQueryList(false));
|
|
48
|
+
});
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
vi.restoreAllMocks();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Note: SSR testing is complex due to static imports, focusing on browser behavior
|
|
54
|
+
|
|
55
|
+
describe('Browser environment', () => {
|
|
56
|
+
it('creates media queries with correct breakpoint values', () => {
|
|
57
|
+
const mockMQL = createMockMediaQueryList(false);
|
|
58
|
+
mockMatchMedia.mockReturnValue(mockMQL);
|
|
59
|
+
renderHook(() => useMedia(['mobile', 'tablet']), {
|
|
60
|
+
wrapper: createWrapper()
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Should be called twice for each query, once for initial and once for effect
|
|
64
|
+
expect(mockMatchMedia).toHaveBeenCalledWith('(min-width: 0px)'); // mobile
|
|
65
|
+
expect(mockMatchMedia).toHaveBeenCalledWith('(min-width: 768px)'); // tablet
|
|
66
|
+
});
|
|
67
|
+
it('returns initial matches state', () => {
|
|
68
|
+
const mockMobileMatch = createMockMediaQueryList(true);
|
|
69
|
+
const mockTabletMatch = createMockMediaQueryList(false);
|
|
70
|
+
mockMatchMedia.mockReturnValueOnce(mockMobileMatch) // initial mobile
|
|
71
|
+
.mockReturnValueOnce(mockTabletMatch) // initial tablet
|
|
72
|
+
.mockReturnValueOnce(mockMobileMatch) // effect mobile
|
|
73
|
+
.mockReturnValueOnce(mockTabletMatch); // effect tablet
|
|
74
|
+
|
|
75
|
+
const {
|
|
76
|
+
result
|
|
77
|
+
} = renderHook(() => useMedia(['mobile', 'tablet']), {
|
|
78
|
+
wrapper: createWrapper()
|
|
79
|
+
});
|
|
80
|
+
expect(result.current).toEqual([true, false]);
|
|
81
|
+
});
|
|
82
|
+
it('sets up event listeners for media query changes', () => {
|
|
83
|
+
const mockMQL1 = createMockMediaQueryList(false);
|
|
84
|
+
const mockMQL2 = createMockMediaQueryList(false);
|
|
85
|
+
mockMatchMedia.mockReturnValueOnce(mockMQL1).mockReturnValueOnce(mockMQL2).mockReturnValueOnce(mockMQL1).mockReturnValueOnce(mockMQL2);
|
|
86
|
+
renderHook(() => useMedia(['mobile', 'tablet']), {
|
|
87
|
+
wrapper: createWrapper()
|
|
88
|
+
});
|
|
89
|
+
expect(mockMQL1.addEventListener).toHaveBeenCalledWith('change', expect.any(Function), {
|
|
90
|
+
passive: true
|
|
91
|
+
});
|
|
92
|
+
expect(mockMQL2.addEventListener).toHaveBeenCalledWith('change', expect.any(Function), {
|
|
93
|
+
passive: true
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
it('updates state when media query changes', () => {
|
|
97
|
+
const mockMQL = createMockMediaQueryList(false);
|
|
98
|
+
let changeHandler;
|
|
99
|
+
mockMQL.addEventListener.mockImplementation((event, handler) => {
|
|
100
|
+
if (event === 'change') {
|
|
101
|
+
changeHandler = handler;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
mockMatchMedia.mockReturnValue(mockMQL);
|
|
105
|
+
const {
|
|
106
|
+
result
|
|
107
|
+
} = renderHook(() => useMedia(['mobile']), {
|
|
108
|
+
wrapper: createWrapper()
|
|
109
|
+
});
|
|
110
|
+
expect(result.current).toEqual([false]);
|
|
111
|
+
|
|
112
|
+
// Simulate media query change
|
|
113
|
+
act(() => {
|
|
114
|
+
changeHandler({
|
|
115
|
+
matches: true
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
expect(result.current).toEqual([true]);
|
|
119
|
+
});
|
|
120
|
+
it('handles multiple media query changes independently', () => {
|
|
121
|
+
const mockMQL1 = createMockMediaQueryList(false);
|
|
122
|
+
const mockMQL2 = createMockMediaQueryList(false);
|
|
123
|
+
let changeHandler1;
|
|
124
|
+
let changeHandler2;
|
|
125
|
+
mockMQL1.addEventListener.mockImplementation((event, handler) => {
|
|
126
|
+
if (event === 'change') changeHandler1 = handler;
|
|
127
|
+
});
|
|
128
|
+
mockMQL2.addEventListener.mockImplementation((event, handler) => {
|
|
129
|
+
if (event === 'change') changeHandler2 = handler;
|
|
130
|
+
});
|
|
131
|
+
mockMatchMedia.mockReturnValueOnce(mockMQL1).mockReturnValueOnce(mockMQL2).mockReturnValueOnce(mockMQL1).mockReturnValueOnce(mockMQL2);
|
|
132
|
+
const {
|
|
133
|
+
result
|
|
134
|
+
} = renderHook(() => useMedia(['mobile', 'tablet']), {
|
|
135
|
+
wrapper: createWrapper()
|
|
136
|
+
});
|
|
137
|
+
expect(result.current).toEqual([false, false]);
|
|
138
|
+
|
|
139
|
+
// Change first media query
|
|
140
|
+
act(() => {
|
|
141
|
+
changeHandler1({
|
|
142
|
+
matches: true
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
expect(result.current).toEqual([true, false]);
|
|
146
|
+
|
|
147
|
+
// Change second media query
|
|
148
|
+
act(() => {
|
|
149
|
+
changeHandler2({
|
|
150
|
+
matches: true
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
expect(result.current).toEqual([true, true]);
|
|
154
|
+
});
|
|
155
|
+
it('removes event listeners on unmount', () => {
|
|
156
|
+
const mockMQL = createMockMediaQueryList(false);
|
|
157
|
+
mockMatchMedia.mockReturnValue(mockMQL);
|
|
158
|
+
const {
|
|
159
|
+
unmount
|
|
160
|
+
} = renderHook(() => useMedia(['mobile']), {
|
|
161
|
+
wrapper: createWrapper()
|
|
162
|
+
});
|
|
163
|
+
expect(mockMQL.removeEventListener).not.toHaveBeenCalled();
|
|
164
|
+
unmount();
|
|
165
|
+
expect(mockMQL.removeEventListener).toHaveBeenCalledWith('change', expect.any(Function));
|
|
166
|
+
});
|
|
167
|
+
it('prevents state updates after unmount', () => {
|
|
168
|
+
const mockMQL = createMockMediaQueryList(false);
|
|
169
|
+
let changeHandler;
|
|
170
|
+
mockMQL.addEventListener.mockImplementation((event, handler) => {
|
|
171
|
+
if (event === 'change') changeHandler = handler;
|
|
172
|
+
});
|
|
173
|
+
mockMatchMedia.mockReturnValue(mockMQL);
|
|
174
|
+
const {
|
|
175
|
+
result,
|
|
176
|
+
unmount
|
|
177
|
+
} = renderHook(() => useMedia(['mobile']), {
|
|
178
|
+
wrapper: createWrapper()
|
|
179
|
+
});
|
|
180
|
+
const initialResult = result.current;
|
|
181
|
+
unmount();
|
|
182
|
+
|
|
183
|
+
// Simulate change after unmount - should not update state
|
|
184
|
+
act(() => {
|
|
185
|
+
changeHandler({
|
|
186
|
+
matches: true
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// State should remain unchanged after unmount
|
|
191
|
+
expect(result.current).toBe(initialResult);
|
|
192
|
+
});
|
|
193
|
+
it('updates listeners when queries change', () => {
|
|
194
|
+
const mockMQL = createMockMediaQueryList(false);
|
|
195
|
+
mockMatchMedia.mockReturnValue(mockMQL);
|
|
196
|
+
const {
|
|
197
|
+
rerender
|
|
198
|
+
} = renderHook(({
|
|
199
|
+
queries
|
|
200
|
+
}) => useMedia(queries), {
|
|
201
|
+
wrapper: createWrapper(),
|
|
202
|
+
initialProps: {
|
|
203
|
+
queries: ['mobile']
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Change queries
|
|
208
|
+
rerender({
|
|
209
|
+
queries: ['tablet', 'desktop']
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Should have been called more times now with the new queries
|
|
213
|
+
expect(mockMatchMedia.mock.calls.length).toBeGreaterThan(2);
|
|
214
|
+
});
|
|
215
|
+
it('handles all standard breakpoint keys', () => {
|
|
216
|
+
const allBreakpoints = ['mobile', 'tablet', 'desktop', 'largeDesktop'];
|
|
217
|
+
mockMatchMedia.mockReturnValue(createMockMediaQueryList(false));
|
|
218
|
+
const {
|
|
219
|
+
result
|
|
220
|
+
} = renderHook(() => useMedia(allBreakpoints), {
|
|
221
|
+
wrapper: createWrapper()
|
|
222
|
+
});
|
|
223
|
+
expect(result.current).toHaveLength(4);
|
|
224
|
+
expect(result.current).toEqual([false, false, false, false]);
|
|
225
|
+
|
|
226
|
+
// Verify correct media queries were created
|
|
227
|
+
expect(mockMatchMedia).toHaveBeenCalledWith('(min-width: 0px)'); // mobile
|
|
228
|
+
expect(mockMatchMedia).toHaveBeenCalledWith('(min-width: 768px)'); // tablet
|
|
229
|
+
expect(mockMatchMedia).toHaveBeenCalledWith('(min-width: 1024px)'); // desktop
|
|
230
|
+
expect(mockMatchMedia).toHaveBeenCalledWith('(min-width: 1920px)'); // largeDesktop
|
|
231
|
+
});
|
|
232
|
+
it('handles empty queries array in browser', () => {
|
|
233
|
+
mockMatchMedia.mockReturnValue(createMockMediaQueryList(false));
|
|
234
|
+
const {
|
|
235
|
+
result
|
|
236
|
+
} = renderHook(() => useMedia([]), {
|
|
237
|
+
wrapper: createWrapper()
|
|
238
|
+
});
|
|
239
|
+
expect(result.current).toEqual([]);
|
|
240
|
+
expect(mockMatchMedia).not.toHaveBeenCalled();
|
|
241
|
+
});
|
|
242
|
+
it('maintains referential stability when values do not change', () => {
|
|
243
|
+
const mockMQL = createMockMediaQueryList(false);
|
|
244
|
+
mockMatchMedia.mockReturnValue(mockMQL);
|
|
245
|
+
const {
|
|
246
|
+
result,
|
|
247
|
+
rerender
|
|
248
|
+
} = renderHook(() => useMedia(['mobile']), {
|
|
249
|
+
wrapper: createWrapper()
|
|
250
|
+
});
|
|
251
|
+
const firstResult = result.current;
|
|
252
|
+
rerender();
|
|
253
|
+
|
|
254
|
+
// Should maintain the same reference when values haven't changed
|
|
255
|
+
expect(result.current).toBe(firstResult);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
describe('Edge cases', () => {
|
|
259
|
+
it('handles rapid successive changes', () => {
|
|
260
|
+
const mockMQL = createMockMediaQueryList(false);
|
|
261
|
+
let changeHandler;
|
|
262
|
+
mockMQL.addEventListener.mockImplementation((event, handler) => {
|
|
263
|
+
if (event === 'change') changeHandler = handler;
|
|
264
|
+
});
|
|
265
|
+
mockMatchMedia.mockReturnValue(mockMQL);
|
|
266
|
+
const {
|
|
267
|
+
result
|
|
268
|
+
} = renderHook(() => useMedia(['mobile']), {
|
|
269
|
+
wrapper: createWrapper()
|
|
270
|
+
});
|
|
271
|
+
expect(result.current).toEqual([false]);
|
|
272
|
+
|
|
273
|
+
// Rapid changes
|
|
274
|
+
act(() => {
|
|
275
|
+
changeHandler({
|
|
276
|
+
matches: true
|
|
277
|
+
});
|
|
278
|
+
changeHandler({
|
|
279
|
+
matches: false
|
|
280
|
+
});
|
|
281
|
+
changeHandler({
|
|
282
|
+
matches: true
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
expect(result.current).toEqual([true]);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const selectors: {
|
|
2
|
+
readonly active: "&:active";
|
|
3
|
+
readonly checked: "&:checked, &[data-checked], &[data-selected]";
|
|
4
|
+
readonly disabled: "&:disabled, &[data-disabled]";
|
|
5
|
+
readonly focus: "&:focus, &[data-focus], &[data-focused]";
|
|
6
|
+
readonly focusVisible: "&:focus-visible, &[data-focus-visible]";
|
|
7
|
+
readonly selected: "&[data-selected], &[aria-selected=\"true\"]";
|
|
8
|
+
readonly hover: string;
|
|
9
|
+
readonly hoverNotDisabled: string;
|
|
10
|
+
readonly hoverNotSelected: string;
|
|
11
|
+
readonly focusVisibleNotSelected: string;
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=selectors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selectors.d.ts","sourceRoot":"","sources":["../../lib/styles/selectors.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,SAAS;;;;;;;;;;;CAmBZ,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// === Common selector patterns for consistent interactive states
|
|
4
|
+
// These patterns provide a standardized way to target element states across components and
|
|
5
|
+
// include support for both pseudo-classes and data attributes used by libraries like React Aria
|
|
6
|
+
|
|
7
|
+
export const selectors = {
|
|
8
|
+
active: '&:active',
|
|
9
|
+
checked: '&:checked, &[data-checked], &[data-selected]',
|
|
10
|
+
disabled: '&:disabled, &[data-disabled]',
|
|
11
|
+
focus: '&:focus, &[data-focus], &[data-focused]',
|
|
12
|
+
focusVisible: '&:focus-visible, &[data-focus-visible]',
|
|
13
|
+
selected: '&[data-selected], &[aria-selected="true"]',
|
|
14
|
+
get hover() {
|
|
15
|
+
return `&:hover:not(${this.disabled}), &[data-hover]:not(${this.disabled})`;
|
|
16
|
+
},
|
|
17
|
+
get hoverNotDisabled() {
|
|
18
|
+
return `&:hover:not(${this.disabled}), &[data-hover]:not(${this.disabled})`;
|
|
19
|
+
},
|
|
20
|
+
get hoverNotSelected() {
|
|
21
|
+
return `&:hover:not(${this.disabled}, ${this.selected}), &[data-hover]:not(${this.disabled}, ${this.selected})`;
|
|
22
|
+
},
|
|
23
|
+
get focusVisibleNotSelected() {
|
|
24
|
+
return `&:focus-visible:not(${this.disabled}, ${this.selected}), &[data-focus-visible]:not(${this.disabled}, ${this.selected})`;
|
|
25
|
+
}
|
|
26
|
+
};
|
package/dist/utils/css.d.ts
CHANGED
|
@@ -1,25 +1,3 @@
|
|
|
1
|
-
import type { StyleRule } from '@vanilla-extract/css';
|
|
2
1
|
export declare const cssVarUnwrap: (value: string) => string;
|
|
3
2
|
export declare const getThemeTokenValue: (themeClass?: string | null, token?: string | null) => string;
|
|
4
|
-
export type SelectorStates = 'base' | 'active' | 'checked' | 'selected' | 'disabled' | 'focus' | 'focusVisible' | 'hover' | 'hoverNotFocus' | 'hoverNotSelected';
|
|
5
|
-
export type InteractionStyleProps = Partial<Record<SelectorStates, StyleRule>>;
|
|
6
|
-
export declare const notDisabled = ":not(:disabled,[data-disabled])";
|
|
7
|
-
export declare const notFocused = ":not(:focus,[data-focus],[data-focused])";
|
|
8
|
-
export declare const notSelected = ":not(:checked,[data-checked],[data-selected])";
|
|
9
|
-
/**
|
|
10
|
-
* Creates style variants for different interaction states that can be spread
|
|
11
|
-
* within vanilla-extract `style` object.
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* const buttonStyle = style({
|
|
15
|
-
* backgroundColor: tokens.colours.background.primary,
|
|
16
|
-
* color: tokens.colours.text.primary,
|
|
17
|
-
* ...interactionStyle({
|
|
18
|
-
* initial: { background: 'white', color: 'black' },
|
|
19
|
-
* hover: { background: 'lightgray' },
|
|
20
|
-
* disabled: { opacity: 0.5 }
|
|
21
|
-
* })
|
|
22
|
-
* });
|
|
23
|
-
*/
|
|
24
|
-
export declare const interactionStyle: (selectors: InteractionStyleProps) => StyleRule;
|
|
25
3
|
//# sourceMappingURL=css.d.ts.map
|
package/dist/utils/css.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"css.d.ts","sourceRoot":"","sources":["../../lib/utils/css.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"css.d.ts","sourceRoot":"","sources":["../../lib/utils/css.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,WAIzC,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC9B,aAAa,MAAM,GAAG,IAAI,EAC1B,QAAQ,MAAM,GAAG,IAAI,KACnB,MAQF,CAAC"}
|
package/dist/utils/css.js
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
|
|
4
|
-
import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
|
|
5
|
-
const _excluded = ["base"];
|
|
6
|
-
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
7
|
-
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
8
3
|
const cssVarRegExp = /var\(([^)]+)\)/;
|
|
9
4
|
export const cssVarUnwrap = value => {
|
|
10
5
|
const matches = cssVarRegExp.exec(value);
|
|
@@ -17,51 +12,4 @@ export const getThemeTokenValue = (themeClass, token) => {
|
|
|
17
12
|
const themedElement = document.querySelector(`.${themeClass}`);
|
|
18
13
|
if (!themedElement || !cssVar) return '';
|
|
19
14
|
return ((_getComputedStyle$get = getComputedStyle(themedElement).getPropertyValue(cssVar)) === null || _getComputedStyle$get === void 0 ? void 0 : _getComputedStyle$get.trim()) || '';
|
|
20
|
-
};
|
|
21
|
-
export const notDisabled = ':not(:disabled,[data-disabled])';
|
|
22
|
-
export const notFocused = ':not(:focus,[data-focus],[data-focused])';
|
|
23
|
-
export const notSelected = ':not(:checked,[data-checked],[data-selected])';
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Creates style variants for different interaction states that can be spread
|
|
27
|
-
* within vanilla-extract `style` object.
|
|
28
|
-
*
|
|
29
|
-
* @example
|
|
30
|
-
* const buttonStyle = style({
|
|
31
|
-
* backgroundColor: tokens.colours.background.primary,
|
|
32
|
-
* color: tokens.colours.text.primary,
|
|
33
|
-
* ...interactionStyle({
|
|
34
|
-
* initial: { background: 'white', color: 'black' },
|
|
35
|
-
* hover: { background: 'lightgray' },
|
|
36
|
-
* disabled: { opacity: 0.5 }
|
|
37
|
-
* })
|
|
38
|
-
* });
|
|
39
|
-
*/
|
|
40
|
-
export const interactionStyle = selectors => {
|
|
41
|
-
const {
|
|
42
|
-
base = {}
|
|
43
|
-
} = selectors,
|
|
44
|
-
interactionStyles = _objectWithoutProperties(selectors, _excluded);
|
|
45
|
-
const styles = _objectSpread(_objectSpread({}, base), {}, {
|
|
46
|
-
selectors: {}
|
|
47
|
-
});
|
|
48
|
-
const rules = {
|
|
49
|
-
hover: `&:hover${notDisabled}, &[data-hover]${notDisabled}`,
|
|
50
|
-
hoverNotFocus: `&:hover${notFocused}${notDisabled}, &[data-hover]${notFocused}${notDisabled}`,
|
|
51
|
-
hoverNotSelected: `&:hover${notSelected}${notDisabled}, &[data-hover]${notSelected}${notDisabled}`,
|
|
52
|
-
active: '&:active',
|
|
53
|
-
selected: '&:checked, &[data-checked], &[data-selected]',
|
|
54
|
-
focus: '&:focus, &[data-focus], &[data-focused]',
|
|
55
|
-
focusVisible: '&:focus-visible, &[data-focus-visible]',
|
|
56
|
-
disabled: '&:disabled, &[data-disabled]'
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
// Add selectors for each interaction state that has styles
|
|
60
|
-
Object.entries(interactionStyles).forEach(([state, stateStyles]) => {
|
|
61
|
-
if (state in rules && stateStyles) {
|
|
62
|
-
if (state === 'checked') state = 'selected';
|
|
63
|
-
styles.selectors[rules[state]] = stateStyles;
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
return styles;
|
|
67
15
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"css.spec.d.ts","sourceRoot":"","sources":["../../lib/utils/css.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { cssVarUnwrap, getThemeTokenValue } from "./css.js";
|
|
4
|
+
describe('css utilities', () => {
|
|
5
|
+
describe('cssVarUnwrap', () => {
|
|
6
|
+
it('extracts CSS variable name from var() function', () => {
|
|
7
|
+
expect(cssVarUnwrap('var(--color-primary)')).toBe('--color-primary');
|
|
8
|
+
expect(cssVarUnwrap('var(--spacing-2)')).toBe('--spacing-2');
|
|
9
|
+
expect(cssVarUnwrap('var(--border-radius, 4px)')).toBe('--border-radius, 4px');
|
|
10
|
+
});
|
|
11
|
+
it('returns original value if not a CSS variable', () => {
|
|
12
|
+
expect(cssVarUnwrap('#ff0000')).toBe('#ff0000');
|
|
13
|
+
expect(cssVarUnwrap('16px')).toBe('16px');
|
|
14
|
+
expect(cssVarUnwrap('solid')).toBe('solid');
|
|
15
|
+
});
|
|
16
|
+
it('handles edge cases', () => {
|
|
17
|
+
expect(cssVarUnwrap('')).toBe('');
|
|
18
|
+
expect(cssVarUnwrap('var()')).toBe('var()');
|
|
19
|
+
expect(cssVarUnwrap('var')).toBe('var');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe('getThemeTokenValue', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
// Clean up any existing test elements
|
|
25
|
+
document.querySelectorAll('.test-theme').forEach(el => el.remove());
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
// Clean up test elements
|
|
29
|
+
document.querySelectorAll('.test-theme').forEach(el => el.remove());
|
|
30
|
+
});
|
|
31
|
+
it('returns empty string when themeClass is null/undefined', () => {
|
|
32
|
+
expect(getThemeTokenValue(null, '--color-primary')).toBe('');
|
|
33
|
+
expect(getThemeTokenValue(undefined, '--color-primary')).toBe('');
|
|
34
|
+
});
|
|
35
|
+
it('returns empty string when token is null/undefined', () => {
|
|
36
|
+
expect(getThemeTokenValue('theme-class', null)).toBe('');
|
|
37
|
+
expect(getThemeTokenValue('theme-class')).toBe('');
|
|
38
|
+
});
|
|
39
|
+
it('returns empty string when element is not found', () => {
|
|
40
|
+
expect(getThemeTokenValue('non-existent-theme', '--color-primary')).toBe('');
|
|
41
|
+
});
|
|
42
|
+
it('returns CSS custom property value from themed element', () => {
|
|
43
|
+
// Create a test element with CSS custom properties
|
|
44
|
+
const testElement = document.createElement('div');
|
|
45
|
+
testElement.className = 'test-theme';
|
|
46
|
+
testElement.style.setProperty('--color-primary', '#ff0000');
|
|
47
|
+
testElement.style.setProperty('--spacing-large', '24px');
|
|
48
|
+
document.body.append(testElement);
|
|
49
|
+
expect(getThemeTokenValue('test-theme', 'var(--color-primary)')).toBe('#ff0000');
|
|
50
|
+
expect(getThemeTokenValue('test-theme', '--spacing-large')).toBe('24px');
|
|
51
|
+
});
|
|
52
|
+
it('trims whitespace from computed values', () => {
|
|
53
|
+
const testElement = document.createElement('div');
|
|
54
|
+
testElement.className = 'test-theme';
|
|
55
|
+
testElement.style.setProperty('--color-with-spaces', ' #ff0000 ');
|
|
56
|
+
document.body.append(testElement);
|
|
57
|
+
expect(getThemeTokenValue('test-theme', '--color-with-spaces')).toBe('#ff0000');
|
|
58
|
+
});
|
|
59
|
+
it('returns empty string for non-existent CSS variables', () => {
|
|
60
|
+
const testElement = document.createElement('div');
|
|
61
|
+
testElement.className = 'test-theme';
|
|
62
|
+
document.body.append(testElement);
|
|
63
|
+
expect(getThemeTokenValue('test-theme', '--non-existent-var')).toBe('');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"object.spec.d.ts","sourceRoot":"","sources":["../../lib/utils/object.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { arrayFromKeys } from "./object.js";
|
|
4
|
+
describe('object utilities', () => {
|
|
5
|
+
describe('arrayFromKeys', () => {
|
|
6
|
+
it('returns array of object keys with proper typing', () => {
|
|
7
|
+
const testObj = {
|
|
8
|
+
name: 'John',
|
|
9
|
+
age: 30,
|
|
10
|
+
active: true
|
|
11
|
+
};
|
|
12
|
+
const keys = arrayFromKeys(testObj);
|
|
13
|
+
expect(keys).toEqual(['name', 'age', 'active']);
|
|
14
|
+
expect(keys).toHaveLength(3);
|
|
15
|
+
});
|
|
16
|
+
it('works with empty objects', () => {
|
|
17
|
+
const emptyObj = {};
|
|
18
|
+
const keys = arrayFromKeys(emptyObj);
|
|
19
|
+
expect(keys).toEqual([]);
|
|
20
|
+
expect(keys).toHaveLength(0);
|
|
21
|
+
});
|
|
22
|
+
it('preserves key order', () => {
|
|
23
|
+
const orderedObj = {
|
|
24
|
+
first: 1,
|
|
25
|
+
second: 2,
|
|
26
|
+
third: 3,
|
|
27
|
+
fourth: 4
|
|
28
|
+
};
|
|
29
|
+
const keys = arrayFromKeys(orderedObj);
|
|
30
|
+
expect(keys).toEqual(['first', 'second', 'third', 'fourth']);
|
|
31
|
+
});
|
|
32
|
+
it('works with objects containing different value types', () => {
|
|
33
|
+
const mixedObj = {
|
|
34
|
+
string: 'text',
|
|
35
|
+
number: 42,
|
|
36
|
+
boolean: false,
|
|
37
|
+
array: [1, 2, 3],
|
|
38
|
+
object: {
|
|
39
|
+
nested: true
|
|
40
|
+
},
|
|
41
|
+
nullValue: null,
|
|
42
|
+
undefinedValue: undefined,
|
|
43
|
+
functionValue: () => {}
|
|
44
|
+
};
|
|
45
|
+
const keys = arrayFromKeys(mixedObj);
|
|
46
|
+
expect(keys).toEqual(['string', 'number', 'boolean', 'array', 'object', 'nullValue', 'undefinedValue', 'functionValue']);
|
|
47
|
+
expect(keys).toHaveLength(8);
|
|
48
|
+
});
|
|
49
|
+
it('works with objects having symbol keys (should be excluded)', () => {
|
|
50
|
+
const symbolKey = Symbol('test');
|
|
51
|
+
const objWithSymbol = {
|
|
52
|
+
normalKey: 'value',
|
|
53
|
+
[symbolKey]: 'symbol value'
|
|
54
|
+
};
|
|
55
|
+
const keys = arrayFromKeys(objWithSymbol);
|
|
56
|
+
|
|
57
|
+
// Object.keys() only returns enumerable string keys, not symbols
|
|
58
|
+
expect(keys).toEqual(['normalKey']);
|
|
59
|
+
expect(keys).toHaveLength(1);
|
|
60
|
+
});
|
|
61
|
+
it('works with objects having numeric string keys', () => {
|
|
62
|
+
const numericKeysObj = {
|
|
63
|
+
'0': 'zero',
|
|
64
|
+
'1': 'one',
|
|
65
|
+
'10': 'ten',
|
|
66
|
+
'2': 'two'
|
|
67
|
+
};
|
|
68
|
+
const keys = arrayFromKeys(numericKeysObj);
|
|
69
|
+
|
|
70
|
+
// Object.keys() returns numeric keys in ascending order, then other keys
|
|
71
|
+
expect(keys).toEqual(['0', '1', '2', '10']);
|
|
72
|
+
expect(keys).toHaveLength(4);
|
|
73
|
+
});
|
|
74
|
+
it('works with objects created with null prototype', () => {
|
|
75
|
+
const nullProtoObj = Object.create(null);
|
|
76
|
+
nullProtoObj.key1 = 'value1';
|
|
77
|
+
nullProtoObj.key2 = 'value2';
|
|
78
|
+
const keys = arrayFromKeys(nullProtoObj);
|
|
79
|
+
expect(keys).toEqual(['key1', 'key2']);
|
|
80
|
+
expect(keys).toHaveLength(2);
|
|
81
|
+
});
|
|
82
|
+
it('ignores inherited properties', () => {
|
|
83
|
+
// Create a prototype object
|
|
84
|
+
const proto = {
|
|
85
|
+
inheritedProp: 'inherited'
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Create object that inherits from proto
|
|
89
|
+
const obj = Object.create(proto);
|
|
90
|
+
obj.ownProp1 = 'own1';
|
|
91
|
+
obj.ownProp2 = 'own2';
|
|
92
|
+
const keys = arrayFromKeys(obj);
|
|
93
|
+
|
|
94
|
+
// Should only include own properties, not inherited ones
|
|
95
|
+
expect(keys).toEqual(['ownProp1', 'ownProp2']);
|
|
96
|
+
expect(keys).toHaveLength(2);
|
|
97
|
+
});
|
|
98
|
+
it('works with array-like objects', () => {
|
|
99
|
+
const arrayLikeObj = {
|
|
100
|
+
'0': 'a',
|
|
101
|
+
'1': 'b',
|
|
102
|
+
'2': 'c',
|
|
103
|
+
length: 3
|
|
104
|
+
};
|
|
105
|
+
const keys = arrayFromKeys(arrayLikeObj);
|
|
106
|
+
|
|
107
|
+
// Array-like objects have numeric string keys + length
|
|
108
|
+
expect(keys).toEqual(['0', '1', '2', 'length']);
|
|
109
|
+
expect(keys).toHaveLength(4);
|
|
110
|
+
});
|
|
111
|
+
it('maintains type safety with specific object types', () => {
|
|
112
|
+
const typedObj = {
|
|
113
|
+
prop1: 'test',
|
|
114
|
+
prop2: 123
|
|
115
|
+
};
|
|
116
|
+
const keys = arrayFromKeys(typedObj);
|
|
117
|
+
|
|
118
|
+
// TypeScript should infer the correct key type
|
|
119
|
+
expect(keys).toEqual(['prop1', 'prop2']);
|
|
120
|
+
|
|
121
|
+
// This ensures the return type is properly typed
|
|
122
|
+
const firstKey = keys[0]; // Should be string
|
|
123
|
+
expect(['prop1', 'prop2']).toContain(firstKey);
|
|
124
|
+
});
|
|
125
|
+
it('works with readonly objects', () => {
|
|
126
|
+
const readonlyObj = {
|
|
127
|
+
readonly1: 'value1',
|
|
128
|
+
readonly2: 'value2'
|
|
129
|
+
};
|
|
130
|
+
const keys = arrayFromKeys(readonlyObj);
|
|
131
|
+
expect(keys).toEqual(['readonly1', 'readonly2']);
|
|
132
|
+
expect(keys).toHaveLength(2);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"responsiveStyle.spec.d.ts","sourceRoot":"","sources":["../../lib/utils/responsiveStyle.spec.ts"],"names":[],"mappings":""}
|