vueless 1.5.2-beta.1 → 1.5.2-beta.3
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/composables/useUI.ts +29 -4
- package/package.json +1 -1
- package/ui.button-toggle/tests/UToggle.test.ts +17 -0
- package/utils/tests/ui.test.ts +8 -30
- package/utils/ui.ts +7 -28
package/composables/useUI.ts
CHANGED
|
@@ -53,7 +53,9 @@ export function useUI<T>(defaultConfig: T, mutatedProps?: MutatedProps, topLevel
|
|
|
53
53
|
|
|
54
54
|
/* Hoist shared reactive primitives — create once, share across all keys. */
|
|
55
55
|
const attrs = useAttrs() as KeyAttrs;
|
|
56
|
-
|
|
56
|
+
|
|
57
|
+
/* Reading `class` explicitly keeps attrs tracked even when they start empty. */
|
|
58
|
+
const reactiveAttrs = computed(() => ({ class: attrs.class, ...attrs }));
|
|
57
59
|
|
|
58
60
|
/**
|
|
59
61
|
* Reactive wrapper for props — created once per component instead of once per key.
|
|
@@ -235,6 +237,7 @@ export function useUI<T>(defaultConfig: T, mutatedProps?: MutatedProps, topLevel
|
|
|
235
237
|
* body row of a table on an unrelated sticky toggle). We skip the `.value` write
|
|
236
238
|
* when the new object is structurally equal to the last one, so the ref keeps its
|
|
237
239
|
* identity and dependents are not re-rendered.
|
|
240
|
+
* `JSON.stringify` drops functions, so listeners are compared by reference separately.
|
|
238
241
|
*/
|
|
239
242
|
const attrsSignatures: Record<string, string> = {};
|
|
240
243
|
|
|
@@ -250,10 +253,10 @@ export function useUI<T>(defaultConfig: T, mutatedProps?: MutatedProps, topLevel
|
|
|
250
253
|
/**
|
|
251
254
|
* Single consolidated watcher instead of N per-key watchers.
|
|
252
255
|
* Watches: config (for config changes), reactiveProps (for prop changes),
|
|
253
|
-
* mutatedProps (for slot/computed prop changes),
|
|
256
|
+
* mutatedProps (for slot/computed prop changes), reactiveAttrs (for attr changes).
|
|
254
257
|
*/
|
|
255
258
|
watch(
|
|
256
|
-
[config, reactiveProps, mutatedProps || (() => undefined),
|
|
259
|
+
[config, reactiveProps, mutatedProps || (() => undefined), reactiveAttrs],
|
|
257
260
|
() => {
|
|
258
261
|
const derived = configDerivedData.value;
|
|
259
262
|
|
|
@@ -285,8 +288,10 @@ export function useUI<T>(defaultConfig: T, mutatedProps?: MutatedProps, topLevel
|
|
|
285
288
|
|
|
286
289
|
/* Keep the previous ref identity when nothing changed — see attrsSignatures. */
|
|
287
290
|
const signature = JSON.stringify(nextValue);
|
|
291
|
+
const isUnchanged =
|
|
292
|
+
attrsSignatures[key] === signature && hasSameListeners(attrsRefs[key].value, nextValue);
|
|
288
293
|
|
|
289
|
-
if (
|
|
294
|
+
if (isUnchanged) continue;
|
|
290
295
|
|
|
291
296
|
attrsSignatures[key] = signature;
|
|
292
297
|
attrsRefs[key].value = nextValue;
|
|
@@ -312,6 +317,26 @@ export function useUI<T>(defaultConfig: T, mutatedProps?: MutatedProps, topLevel
|
|
|
312
317
|
return { config, getDataTest, ...getKeysAttrs(mutatedProps) } as UseUI<T>;
|
|
313
318
|
}
|
|
314
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Check both attrs objects hold the same function-valued entries (listeners) by reference.
|
|
322
|
+
*/
|
|
323
|
+
function hasSameListeners(previous: KeyAttrs, next: KeyAttrs): boolean {
|
|
324
|
+
let listenersDiff = 0;
|
|
325
|
+
|
|
326
|
+
for (const key in next) {
|
|
327
|
+
if (typeof next[key] !== "function") continue;
|
|
328
|
+
if (next[key] !== previous[key]) return false;
|
|
329
|
+
|
|
330
|
+
listenersDiff++;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
for (const key in previous) {
|
|
334
|
+
if (typeof previous[key] === "function") listenersDiff--;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return listenersDiff === 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
315
340
|
/**
|
|
316
341
|
* Return base classes.
|
|
317
342
|
*/
|
package/package.json
CHANGED
|
@@ -395,6 +395,23 @@ describe("UToggle.vue", () => {
|
|
|
395
395
|
expect.objectContaining({ value: "option1", label: "Option 1" }),
|
|
396
396
|
);
|
|
397
397
|
});
|
|
398
|
+
|
|
399
|
+
it("Options – emits the new option value after options are swapped", async () => {
|
|
400
|
+
const component = mount(UToggle, {
|
|
401
|
+
props: {
|
|
402
|
+
options: [{ value: "code" }, { value: "self" }],
|
|
403
|
+
name,
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
await component.setProps({
|
|
408
|
+
options: [{ value: "git" }, { value: "jira" }, { value: "slack" }],
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
await component.findAllComponents(UButton)[1].trigger("click");
|
|
412
|
+
|
|
413
|
+
expect(component.emitted("update:modelValue")?.[0][0]).toBe("jira");
|
|
414
|
+
});
|
|
398
415
|
});
|
|
399
416
|
|
|
400
417
|
describe("Exposed refs", () => {
|
package/utils/tests/ui.test.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { describe, it, expect, vi
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
2
|
|
|
3
3
|
import { COLOR_MODE_KEY } from "../../constants.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
import type { Config } from "../../types";
|
|
4
|
+
import { setVuelessConfig } from "../ui";
|
|
7
5
|
|
|
8
6
|
/* jsdom doesn't implement matchMedia, which theme.ts evaluates on import. */
|
|
9
7
|
const prefersDark = { matches: false, addEventListener: vi.fn(), removeEventListener: vi.fn() };
|
|
@@ -12,37 +10,17 @@ window.matchMedia = vi.fn(() => prefersDark) as unknown as typeof window.matchMe
|
|
|
12
10
|
|
|
13
11
|
const { getThemeCookieName } = await import("../theme");
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
describe("loadSSRVuelessConfig", () => {
|
|
20
|
-
it.each([
|
|
21
|
-
["resolves", () => ({ cookiePrefix: "from-file" })],
|
|
22
|
-
[
|
|
23
|
-
"fails",
|
|
24
|
-
() => {
|
|
25
|
-
throw new Error("Cannot find module '/vueless.config'");
|
|
26
|
-
},
|
|
27
|
-
],
|
|
28
|
-
])("keeps the injected config when a deferred import %s", async (_, settle: () => Config) => {
|
|
29
|
-
let release!: () => void;
|
|
30
|
-
const released = new Promise<void>((resolve) => (release = resolve));
|
|
31
|
-
|
|
32
|
-
const loading = loadSSRVuelessConfig(() => released.then(() => ({ default: settle() })));
|
|
33
|
-
|
|
13
|
+
describe("setVuelessConfig", () => {
|
|
14
|
+
it("uses the injected config", () => {
|
|
34
15
|
setVuelessConfig({ cookiePrefix: "app-a" });
|
|
35
|
-
expect(getThemeCookieName(COLOR_MODE_KEY)).toBe(`app-a-${COLOR_MODE_KEY}`);
|
|
36
|
-
|
|
37
|
-
release();
|
|
38
|
-
await loading;
|
|
39
16
|
|
|
40
17
|
expect(getThemeCookieName(COLOR_MODE_KEY)).toBe(`app-a-${COLOR_MODE_KEY}`);
|
|
41
18
|
});
|
|
42
19
|
|
|
43
|
-
it("
|
|
44
|
-
|
|
20
|
+
it("keeps the current config when an empty one is injected", () => {
|
|
21
|
+
setVuelessConfig({ cookiePrefix: "app-a" });
|
|
22
|
+
setVuelessConfig({});
|
|
45
23
|
|
|
46
|
-
expect(getThemeCookieName(COLOR_MODE_KEY)).toBe(`
|
|
24
|
+
expect(getThemeCookieName(COLOR_MODE_KEY)).toBe(`app-a-${COLOR_MODE_KEY}`);
|
|
47
25
|
});
|
|
48
26
|
});
|
package/utils/ui.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { cx as cvaCx } from "cva";
|
|
|
3
3
|
import { defineConfig } from "cva/config";
|
|
4
4
|
import { extendTailwindMerge } from "tailwind-merge";
|
|
5
5
|
|
|
6
|
-
import { isCSR, isSSR } from "./helper";
|
|
7
6
|
import { createGetMergedConfig } from "./node/mergeConfigs";
|
|
8
7
|
import { COMPONENT_NAME as U_ICON } from "../ui.image-icon/constants";
|
|
9
8
|
import { ICON_NON_PROPS_DEFAULTS, TAILWIND_MERGE_EXTENSION } from "../constants";
|
|
@@ -30,7 +29,13 @@ type GetMergedConfig = (options: MergedConfigOptions) => unknown;
|
|
|
30
29
|
* Load Vueless config from the project root.
|
|
31
30
|
* Both for server and client side renderings.
|
|
32
31
|
*/
|
|
33
|
-
export let vuelessConfig: Config =
|
|
32
|
+
export let vuelessConfig: Config =
|
|
33
|
+
Object.values(
|
|
34
|
+
import.meta.glob(["/vueless.config.{js,ts}"], {
|
|
35
|
+
eager: true,
|
|
36
|
+
import: "default",
|
|
37
|
+
}),
|
|
38
|
+
)[0] || {};
|
|
34
39
|
|
|
35
40
|
export function setVuelessConfig(config?: Config) {
|
|
36
41
|
config = config || {};
|
|
@@ -38,32 +43,6 @@ export function setVuelessConfig(config?: Config) {
|
|
|
38
43
|
vuelessConfig = Object.keys(config).length ? config : vuelessConfig;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
if (isCSR) {
|
|
42
|
-
vuelessConfig =
|
|
43
|
-
Object.values(
|
|
44
|
-
import.meta.glob(["/vueless.config.{js,ts}"], {
|
|
45
|
-
eager: true,
|
|
46
|
-
import: "default",
|
|
47
|
-
}),
|
|
48
|
-
)[0] || {};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Load Vueless config on the server; a config injected via `createVueless` meanwhile wins. */
|
|
52
|
-
export async function loadSSRVuelessConfig(
|
|
53
|
-
// @ts-expect-error: vueless.config.{js,ts} is optional
|
|
54
|
-
importConfig = () => import(/* @vite-ignore */ "/vueless.config"),
|
|
55
|
-
) {
|
|
56
|
-
try {
|
|
57
|
-
const { default: config } = await importConfig();
|
|
58
|
-
|
|
59
|
-
if (!Object.keys(vuelessConfig).length) vuelessConfig = config;
|
|
60
|
-
} catch {
|
|
61
|
-
/* No config file: keep the current config. */
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (isSSR) loadSSRVuelessConfig();
|
|
66
|
-
|
|
67
46
|
/**
|
|
68
47
|
* Extend twMerge (tailwind merge) by vueless and user config:
|
|
69
48
|
*/
|