From 1d2130f2791e2612dfdff870c05616947d927b4b Mon Sep 17 00:00:00 2001 From: robonen Date: Mon, 10 Aug 2026 00:14:44 +0700 Subject: [PATCH] fix(primitives): make v-model, native attributes and panel styling usable under strict TS Consuming the package under `verbatimModuleSyntax` + `strictTemplates` surfaced four defects that forced workarounds downstream. - Drop the deprecated `SelectValue` string alias. It collided with the `SelectValue` component exported from the same barrel, so the component resolved to the type meaning and could not be imported (TS1484). - Narrow the select's model to `SelectModelValue` and make `TabsRoot` generic over its value, so a plain `v-model` on a `Ref` type-checks. Both roots declare the value prop and emit explicitly instead of via `defineModel`, which would widen the payload with `| undefined` even though neither control ever clears its value. - Stop declaring `defineModel` keys in `defineEmits` as well. The duplicate erased the payload type from the generated declarations, shipping `(...args: unknown[]) => any` for eleven components' model events. - Let every part accept global DOM attributes (`id`, `role`, `aria-*`, `data-*`, ...) through `PrimitiveAttributes`. The heritage clause is marked `@vue-ignore`, so they stay out of the runtime props and keep falling through via `$attrs` exactly as before. - Give `SelectContent` and `SelectViewport` a single styleable root: the content forwards `$attrs` onto the panel, and the viewport's scrollbar CSS moves to a reference-counted `` style tag. A forwarded `class` was previously dropped, leaving the panel unstyled. The tsconfig vue preset gains `htmlAttributes: ["aria-*", "data-*"]` so hyphenated data attributes are not camelized before they reach those types. Co-Authored-By: Claude Opus 5 --- configs/tsconfig/package.json | 2 +- configs/tsconfig/tsconfig.vue.json | 1 + vue/primitives/jsr.json | 2 +- vue/primitives/package.json | 2 +- .../disclosure/accordion/AccordionRoot.vue | 9 ++- .../src/disclosure/tabs/TabsRoot.vue | 40 ++++++---- .../src/display/calendar/CalendarRoot.vue | 7 +- .../display/date-picker/DatePickerRoot.vue | 7 +- .../src/display/progress/ProgressRoot.vue | 7 +- vue/primitives/src/forms/switch/Switch.vue | 7 +- vue/primitives/src/forms/toggle/Toggle.vue | 8 +- .../src/internal/primitive/Primitive.ts | 30 +++++++- .../src/internal/primitive/index.ts | 2 +- .../navigation-menu/NavigationMenuRoot.vue | 7 +- .../navigation-menu/NavigationMenuSub.vue | 7 +- .../src/menus/toolbar/ToolbarRoot.vue | 10 ++- .../src/selection/select/SelectContent.vue | 7 +- .../src/selection/select/SelectRoot.vue | 74 +++++++++++++------ .../src/selection/select/SelectViewport.vue | 13 ++-- .../selection/select/__test__/Select.test.ts | 71 ++++++++++++++++++ .../src/selection/select/context.ts | 7 -- vue/primitives/src/selection/select/index.ts | 3 +- vue/primitives/src/selection/select/utils.ts | 5 ++ .../roving-focus/RovingFocusGroup.vue | 10 ++- 24 files changed, 260 insertions(+), 78 deletions(-) diff --git a/configs/tsconfig/package.json b/configs/tsconfig/package.json index 9a638c8..6b05f30 100644 --- a/configs/tsconfig/package.json +++ b/configs/tsconfig/package.json @@ -1,6 +1,6 @@ { "name": "@robonen/tsconfig", - "version": "0.1.0", + "version": "0.1.1", "license": "Apache-2.0", "description": "Base typescript configuration for projects", "keywords": [ diff --git a/configs/tsconfig/tsconfig.vue.json b/configs/tsconfig/tsconfig.vue.json index 59c79f9..299f107 100644 --- a/configs/tsconfig/tsconfig.vue.json +++ b/configs/tsconfig/tsconfig.vue.json @@ -8,6 +8,7 @@ "vueCompilerOptions": { "strictTemplates": true, "fallthroughAttributes": true, + "htmlAttributes": ["aria-*", "data-*"], "inferTemplateDollarAttrs": true, "inferTemplateDollarEl": true, "inferTemplateDollarRefs": true diff --git a/vue/primitives/jsr.json b/vue/primitives/jsr.json index aaae1da..9a289c1 100644 --- a/vue/primitives/jsr.json +++ b/vue/primitives/jsr.json @@ -2,6 +2,6 @@ "$schema": "https://jsr.io/schema/config-file.v1.json", "name": "@robonen/primitives", "license": "Apache-2.0", - "version": "0.0.2", + "version": "0.0.3", "exports": "./src/index.ts" } diff --git a/vue/primitives/package.json b/vue/primitives/package.json index 1a37a57..6d85264 100644 --- a/vue/primitives/package.json +++ b/vue/primitives/package.json @@ -1,6 +1,6 @@ { "name": "@robonen/primitives", - "version": "0.0.2", + "version": "0.0.3", "license": "Apache-2.0", "description": "Collection of UI primitives", "keywords": [ diff --git a/vue/primitives/src/disclosure/accordion/AccordionRoot.vue b/vue/primitives/src/disclosure/accordion/AccordionRoot.vue index 1b81665..dd07c46 100644 --- a/vue/primitives/src/disclosure/accordion/AccordionRoot.vue +++ b/vue/primitives/src/disclosure/accordion/AccordionRoot.vue @@ -14,6 +14,9 @@ import type { RovingDirection } from '../../internal/utils/roving-focus'; export type AccordionType = 'single' | 'multiple'; export interface AccordionRootProps extends PrimitiveProps { + /** Controlled open value(s). Bind with `v-model`. */ + modelValue?: string | string[]; + /** Initial value(s) for uncontrolled mode. */ defaultValue?: string | string[]; @@ -51,6 +54,10 @@ export interface AccordionRootProps extends PrimitiveProps { /** * Emit contract for `AccordionRoot`. The payload narrows with `Type`: a single * accordion emits `string | undefined`, a multiple accordion emits `string[]`. + * + * The event itself is declared by `defineModel`: passing a model key through + * `defineEmits` as well erases its payload type from the generated + * declarations, leaving consumers with `unknown`. */ export interface AccordionRootEmits { 'update:modelValue': [value: (Type extends 'single' ? string : string[]) | undefined]; @@ -79,8 +86,6 @@ const { as = 'div', } = defineProps(); -defineEmits(); - defineSlots<{ default?: (props: { /** Current open value(s): a `string | undefined` in single mode, `string[]` in multiple. */ diff --git a/vue/primitives/src/disclosure/tabs/TabsRoot.vue b/vue/primitives/src/disclosure/tabs/TabsRoot.vue index bff3ef2..c09b882 100644 --- a/vue/primitives/src/disclosure/tabs/TabsRoot.vue +++ b/vue/primitives/src/disclosure/tabs/TabsRoot.vue @@ -13,11 +13,11 @@ import type { TabsValue } from './context'; * via `defaultValue`), orientation, keyboard roving focus across triggers, and * provides context to every `TabsList`, `TabsTrigger`, and `TabsContent`. */ -export interface TabsRootProps extends PrimitiveProps { +export interface TabsRootProps extends PrimitiveProps { /** Controlled selected value. Bind with `v-model`. */ - modelValue?: TabsValue; + modelValue?: Value; /** Uncontrolled initial value. */ - defaultValue?: TabsValue; + defaultValue?: Value; /** Orientation of the tab list. @default 'horizontal' */ orientation?: 'horizontal' | 'vertical'; /** @@ -40,13 +40,14 @@ export interface TabsRootProps extends PrimitiveProps { unmountOnHide?: boolean; } -export interface TabsRootEmits { +export interface TabsRootEmits { /** Fired when the selected value changes. */ - 'update:modelValue': [value: TabsValue | undefined]; + 'update:modelValue': [value: Value]; } - -