From def1db8b6cb038e2019df96076d24e02c3526cc6 Mon Sep 17 00:00:00 2001 From: robonen Date: Mon, 8 Jun 2026 15:51:30 +0700 Subject: [PATCH] fix(primitives): component fixes and test cleanup --- .../12dbc42b176f989336a161e4f129aeb48944d8d4.png | Bin 4774 -> 0 bytes vue/primitives/src/avatar/AvatarImage.vue | 2 +- vue/primitives/src/combobox/ComboboxRoot.vue | 3 +-- vue/primitives/src/command/CommandRoot.vue | 2 +- vue/primitives/src/env.d.ts | 8 ++------ vue/primitives/src/hover-card/HoverCardRoot.vue | 3 +++ .../src/navigation-menu/NavigationMenuList.vue | 3 +++ .../src/navigation-menu/NavigationMenuRoot.vue | 5 ++--- .../src/navigation-menu/NavigationMenuSub.vue | 3 +-- vue/primitives/src/pin-input/PinInputInput.vue | 2 +- .../src/tags-input/TagsInputItemDelete.vue | 2 +- .../src/tags-input/__test__/TagsInput.test.ts | 4 ++-- 12 files changed, 18 insertions(+), 19 deletions(-) delete mode 100644 vue/primitives/.vitest-attachments/12dbc42b176f989336a161e4f129aeb48944d8d4.png diff --git a/vue/primitives/.vitest-attachments/12dbc42b176f989336a161e4f129aeb48944d8d4.png b/vue/primitives/.vitest-attachments/12dbc42b176f989336a161e4f129aeb48944d8d4.png deleted file mode 100644 index d1c5f0892e4109e688d20597df90050b15aaf93f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4774 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 diff --git a/vue/primitives/src/avatar/AvatarImage.vue b/vue/primitives/src/avatar/AvatarImage.vue index ce262b9..3f8b0f7 100644 --- a/vue/primitives/src/avatar/AvatarImage.vue +++ b/vue/primitives/src/avatar/AvatarImage.vue @@ -43,7 +43,7 @@ function load(nextSrc: string | undefined) { setStatus('error'); return; } - if (typeof globalThis.window === 'undefined') { + if (globalThis.window === undefined) { setStatus('loading'); return; } diff --git a/vue/primitives/src/combobox/ComboboxRoot.vue b/vue/primitives/src/combobox/ComboboxRoot.vue index 59d0eb3..5b45c9b 100644 --- a/vue/primitives/src/combobox/ComboboxRoot.vue +++ b/vue/primitives/src/combobox/ComboboxRoot.vue @@ -7,8 +7,6 @@ export interface ComboboxRootProps modelValue?: T | T[]; /** Uncontrolled initial value. */ defaultValue?: T | T[]; - /** Controlled open state. Use `v-model:open`. */ - open?: boolean; /** Uncontrolled default open state. */ defaultOpen?: boolean; /** Allow selecting multiple values. */ @@ -77,6 +75,7 @@ const config = useConfig(); const direction = computed(() => dir ?? config.dir.value); const localOpen = ref(defaultOpen); +/** Controlled open state. Use `v-model:open`. */ const open = defineModel('open', { default: undefined, get: v => v ?? localOpen.value, diff --git a/vue/primitives/src/command/CommandRoot.vue b/vue/primitives/src/command/CommandRoot.vue index 8df58c2..ad41aac 100644 --- a/vue/primitives/src/command/CommandRoot.vue +++ b/vue/primitives/src/command/CommandRoot.vue @@ -138,7 +138,7 @@ const filteredItems = computed>(() => { function escapeAttr(v: string): string { if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') return CSS.escape(v); - return v.replace(/["\\]/g, '\\$&'); + return v.replaceAll(/["\\]/g, '\\$&'); } function getSelectableItems(): string[] { diff --git a/vue/primitives/src/env.d.ts b/vue/primitives/src/env.d.ts index b9d14ca..71a152f 100644 --- a/vue/primitives/src/env.d.ts +++ b/vue/primitives/src/env.d.ts @@ -5,13 +5,9 @@ declare global { } declare module 'vue' { - interface ComponentCustomProps { - [key: `data${string}`]: unknown; - } + type ComponentCustomProps = Record<`data${string}`, unknown>; } declare module 'vue' { - interface HTMLAttributes { - [key: `data-${string}`]: unknown; - } + type HTMLAttributes = Record<`data-${string}`, unknown>; } diff --git a/vue/primitives/src/hover-card/HoverCardRoot.vue b/vue/primitives/src/hover-card/HoverCardRoot.vue index 09feb72..402c063 100644 --- a/vue/primitives/src/hover-card/HoverCardRoot.vue +++ b/vue/primitives/src/hover-card/HoverCardRoot.vue @@ -21,6 +21,9 @@ const { openDelay = 700, closeDelay = 300, defaultOpen = false } = defineProps('open', { default: undefined }); const uncontrolled = ref(defaultOpen); +// `open` intentionally shares the model name: it's a local read-only computed that +// resolves the controlled model against the uncontrolled fallback. Safe in script-setup. +// eslint-disable-next-line vue/no-dupe-keys const open = computed(() => openModel.value ?? uncontrolled.value); function setOpen(value: boolean) { diff --git a/vue/primitives/src/navigation-menu/NavigationMenuList.vue b/vue/primitives/src/navigation-menu/NavigationMenuList.vue index 8b36fc2..381d372 100644 --- a/vue/primitives/src/navigation-menu/NavigationMenuList.vue +++ b/vue/primitives/src/navigation-menu/NavigationMenuList.vue @@ -1,4 +1,7 @@ diff --git a/vue/primitives/src/navigation-menu/NavigationMenuRoot.vue b/vue/primitives/src/navigation-menu/NavigationMenuRoot.vue index 727d140..c4f56b4 100644 --- a/vue/primitives/src/navigation-menu/NavigationMenuRoot.vue +++ b/vue/primitives/src/navigation-menu/NavigationMenuRoot.vue @@ -3,8 +3,6 @@ import type { Direction } from '../config-provider'; import type { Orientation } from '../roving-focus'; export interface NavigationMenuRootProps { - /** Controlled active item value. Use `v-model`. */ - modelValue?: string; /** Uncontrolled initial value. */ defaultValue?: string; /** Reading direction. Falls back to `ConfigProvider`. */ @@ -72,6 +70,7 @@ const config = useConfig(); const dirRef = computed(() => dir ?? config.dir.value); const localValue = ref(defaultValue ?? ''); +/** Controlled active item value. Use `v-model`. */ const modelValue = defineModel({ default: undefined, get: v => v ?? localValue.value, @@ -212,7 +211,7 @@ provideNavigationMenuContext({ (); const localValue = ref(defaultValue ?? ''); +/** Controlled active value of the submenu. Use `v-model`. */ const modelValue = defineModel({ default: undefined, get: v => v ?? localValue.value, diff --git a/vue/primitives/src/pin-input/PinInputInput.vue b/vue/primitives/src/pin-input/PinInputInput.vue index fa0cc5d..814d8a5 100644 --- a/vue/primitives/src/pin-input/PinInputInput.vue +++ b/vue/primitives/src/pin-input/PinInputInput.vue @@ -102,7 +102,7 @@ function onPaste(e: ClipboardEvent): void { return; e.preventDefault(); const chars = ctx.type.value === 'number' - ? data.replace(NON_DIGIT_G, '').split('') + ? data.replaceAll(NON_DIGIT_G, '').split('') : data.split(''); let idx = props.index; for (const ch of chars) { diff --git a/vue/primitives/src/tags-input/TagsInputItemDelete.vue b/vue/primitives/src/tags-input/TagsInputItemDelete.vue index 2b9b350..ee5b05d 100644 --- a/vue/primitives/src/tags-input/TagsInputItemDelete.vue +++ b/vue/primitives/src/tags-input/TagsInputItemDelete.vue @@ -17,7 +17,7 @@ const { forwardRef } = useForwardExpose(); function onClick(): void { if (item.disabled.value) return; - const idx = ctx.modelValue.value.findIndex(v => v === item.value.value); + const idx = ctx.modelValue.value.indexOf(item.value.value); if (idx !== -1) ctx.onRemoveValue(idx); } diff --git a/vue/primitives/src/tags-input/__test__/TagsInput.test.ts b/vue/primitives/src/tags-input/__test__/TagsInput.test.ts index 5d66983..d53918c 100644 --- a/vue/primitives/src/tags-input/__test__/TagsInput.test.ts +++ b/vue/primitives/src/tags-input/__test__/TagsInput.test.ts @@ -120,8 +120,8 @@ describe('TagsInput', () => { it('delete trigger removes its tag', async () => { const w = createTagsInput({ defaultValue: ['a', 'b'] }); - const deleteBtns = w.findAll('button').filter(b => b.text() === '×'); - await deleteBtns[0]!.trigger('click'); + const deleteBtn = w.findAll('button').find(b => b.text() === '×'); + await deleteBtn!.trigger('click'); await nextTick(); const items = w.findAllComponents(TagsInputItem as Component); expect(items).toHaveLength(1);