fix(primitives): make v-model, native attributes and panel styling usable under strict TS
Publish to NPM / Check version changes and publish (push) Successful in 11m33s
Publish to NPM / Check version changes and publish (push) Successful in 11m33s
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<T, Multiple>` and make `TabsRoot` generic over its value, so a plain `v-model` on a `Ref<string>` 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 `<head>` 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 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@robonen/tsconfig",
|
"name": "@robonen/tsconfig",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Base typescript configuration for projects",
|
"description": "Base typescript configuration for projects",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"vueCompilerOptions": {
|
"vueCompilerOptions": {
|
||||||
"strictTemplates": true,
|
"strictTemplates": true,
|
||||||
"fallthroughAttributes": true,
|
"fallthroughAttributes": true,
|
||||||
|
"htmlAttributes": ["aria-*", "data-*"],
|
||||||
"inferTemplateDollarAttrs": true,
|
"inferTemplateDollarAttrs": true,
|
||||||
"inferTemplateDollarEl": true,
|
"inferTemplateDollarEl": true,
|
||||||
"inferTemplateDollarRefs": true
|
"inferTemplateDollarRefs": true
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
"$schema": "https://jsr.io/schema/config-file.v1.json",
|
"$schema": "https://jsr.io/schema/config-file.v1.json",
|
||||||
"name": "@robonen/primitives",
|
"name": "@robonen/primitives",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"version": "0.0.2",
|
"version": "0.0.3",
|
||||||
"exports": "./src/index.ts"
|
"exports": "./src/index.ts"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@robonen/primitives",
|
"name": "@robonen/primitives",
|
||||||
"version": "0.0.2",
|
"version": "0.0.3",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Collection of UI primitives",
|
"description": "Collection of UI primitives",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import type { RovingDirection } from '../../internal/utils/roving-focus';
|
|||||||
export type AccordionType = 'single' | 'multiple';
|
export type AccordionType = 'single' | 'multiple';
|
||||||
|
|
||||||
export interface AccordionRootProps extends PrimitiveProps {
|
export interface AccordionRootProps extends PrimitiveProps {
|
||||||
|
/** Controlled open value(s). Bind with `v-model`. */
|
||||||
|
modelValue?: string | string[];
|
||||||
|
|
||||||
/** Initial value(s) for uncontrolled mode. */
|
/** Initial value(s) for uncontrolled mode. */
|
||||||
defaultValue?: string | string[];
|
defaultValue?: string | string[];
|
||||||
|
|
||||||
@@ -51,6 +54,10 @@ export interface AccordionRootProps extends PrimitiveProps {
|
|||||||
/**
|
/**
|
||||||
* Emit contract for `AccordionRoot`. The payload narrows with `Type`: a single
|
* Emit contract for `AccordionRoot`. The payload narrows with `Type`: a single
|
||||||
* accordion emits `string | undefined`, a multiple accordion emits `string[]`.
|
* 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<Type extends AccordionType = AccordionType> {
|
export interface AccordionRootEmits<Type extends AccordionType = AccordionType> {
|
||||||
'update:modelValue': [value: (Type extends 'single' ? string : string[]) | undefined];
|
'update:modelValue': [value: (Type extends 'single' ? string : string[]) | undefined];
|
||||||
@@ -79,8 +86,6 @@ const {
|
|||||||
as = 'div',
|
as = 'div',
|
||||||
} = defineProps<AccordionRootProps>();
|
} = defineProps<AccordionRootProps>();
|
||||||
|
|
||||||
defineEmits<AccordionRootEmits>();
|
|
||||||
|
|
||||||
defineSlots<{
|
defineSlots<{
|
||||||
default?: (props: {
|
default?: (props: {
|
||||||
/** Current open value(s): a `string | undefined` in single mode, `string[]` in multiple. */
|
/** Current open value(s): a `string | undefined` in single mode, `string[]` in multiple. */
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import type { TabsValue } from './context';
|
|||||||
* via `defaultValue`), orientation, keyboard roving focus across triggers, and
|
* via `defaultValue`), orientation, keyboard roving focus across triggers, and
|
||||||
* provides context to every `TabsList`, `TabsTrigger`, and `TabsContent`.
|
* provides context to every `TabsList`, `TabsTrigger`, and `TabsContent`.
|
||||||
*/
|
*/
|
||||||
export interface TabsRootProps extends PrimitiveProps {
|
export interface TabsRootProps<Value extends TabsValue = TabsValue> extends PrimitiveProps {
|
||||||
/** Controlled selected value. Bind with `v-model`. */
|
/** Controlled selected value. Bind with `v-model`. */
|
||||||
modelValue?: TabsValue;
|
modelValue?: Value;
|
||||||
/** Uncontrolled initial value. */
|
/** Uncontrolled initial value. */
|
||||||
defaultValue?: TabsValue;
|
defaultValue?: Value;
|
||||||
/** Orientation of the tab list. @default 'horizontal' */
|
/** Orientation of the tab list. @default 'horizontal' */
|
||||||
orientation?: 'horizontal' | 'vertical';
|
orientation?: 'horizontal' | 'vertical';
|
||||||
/**
|
/**
|
||||||
@@ -40,13 +40,14 @@ export interface TabsRootProps extends PrimitiveProps {
|
|||||||
unmountOnHide?: boolean;
|
unmountOnHide?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TabsRootEmits {
|
export interface TabsRootEmits<Value extends TabsValue = TabsValue> {
|
||||||
/** Fired when the selected value changes. */
|
/** Fired when the selected value changes. */
|
||||||
'update:modelValue': [value: TabsValue | undefined];
|
'update:modelValue': [value: Value];
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts" generic="Value extends TabsValue = TabsValue">
|
||||||
|
import type { Ref } from 'vue';
|
||||||
import { computed, ref, shallowRef, toRef } from 'vue';
|
import { computed, ref, shallowRef, toRef } from 'vue';
|
||||||
import { resolveNextIndex, rovingKeyToAction } from '../../internal/utils/roving-focus';
|
import { resolveNextIndex, rovingKeyToAction } from '../../internal/utils/roving-focus';
|
||||||
import { useCollectionProvider } from '../../utilities/collection';
|
import { useCollectionProvider } from '../../utilities/collection';
|
||||||
@@ -63,15 +64,16 @@ const {
|
|||||||
activationMode = 'automatic',
|
activationMode = 'automatic',
|
||||||
unmountOnHide = true,
|
unmountOnHide = true,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
|
modelValue,
|
||||||
as = 'div',
|
as = 'div',
|
||||||
} = defineProps<TabsRootProps>();
|
} = defineProps<TabsRootProps<Value>>();
|
||||||
|
|
||||||
defineEmits<TabsRootEmits>();
|
const emit = defineEmits<TabsRootEmits<Value>>();
|
||||||
|
|
||||||
defineSlots<{
|
defineSlots<{
|
||||||
default?: (props: {
|
default?: (props: {
|
||||||
/** Current selected value. */
|
/** Current selected value. */
|
||||||
value: TabsValue | undefined;
|
value: Value | undefined;
|
||||||
}) => unknown;
|
}) => unknown;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -79,16 +81,24 @@ const { forwardRef } = useForwardExpose();
|
|||||||
|
|
||||||
const direction = useDirection(() => dir);
|
const direction = useDirection(() => dir);
|
||||||
|
|
||||||
const localValue = ref<TabsValue | undefined>(defaultValue);
|
// `defineModel` would type `update:modelValue` as `TabsValue | undefined`,
|
||||||
|
// forcing every consumer's `v-model` target to accept `undefined` even though
|
||||||
|
// a tab is never deselected. The prop and the emit are declared separately so
|
||||||
|
// the emitted payload stays exactly `TabsValue` (see AGENTS §3.2.3).
|
||||||
|
const localValue = ref<Value | undefined>(defaultValue) as Ref<Value | undefined>;
|
||||||
|
|
||||||
const value = defineModel<TabsValue | undefined>({
|
const value = computed<Value | undefined>({
|
||||||
get: v => v ?? localValue.value,
|
get: () => modelValue ?? localValue.value,
|
||||||
set: (v) => {
|
set: (v) => {
|
||||||
localValue.value = v;
|
localValue.value = v;
|
||||||
return v;
|
if (v !== undefined) emit('update:modelValue', v);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The tab parts read and write plain `TabsValue`s through the context; the
|
||||||
|
// narrowed `Value` only exists to keep the consumer's `v-model` typed.
|
||||||
|
const contextValue = value as unknown as Ref<TabsValue | undefined>;
|
||||||
|
|
||||||
const baseId = useId(undefined, 'tabs');
|
const baseId = useId(undefined, 'tabs');
|
||||||
const tabsListElement = shallowRef<HTMLElement>();
|
const tabsListElement = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
@@ -116,7 +126,7 @@ function unregisterContent(v: TabsValue): void {
|
|||||||
|
|
||||||
function select(v: TabsValue): void {
|
function select(v: TabsValue): void {
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
value.value = v;
|
contextValue.value = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DOM-order tabs via Collection primitive — survives `v-for` reorders and
|
// DOM-order tabs via Collection primitive — survives `v-for` reorders and
|
||||||
@@ -161,7 +171,7 @@ function onTriggerKeyDown(event: KeyboardEvent, el: HTMLElement): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
provideTabsContext({
|
provideTabsContext({
|
||||||
value,
|
value: contextValue,
|
||||||
// Identity passthroughs via `toRef` — reactive without `computed`'s effect/cache.
|
// Identity passthroughs via `toRef` — reactive without `computed`'s effect/cache.
|
||||||
orientation: toRef(() => orientation),
|
orientation: toRef(() => orientation),
|
||||||
direction,
|
direction,
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ export interface CalendarRootProps extends PrimitiveProps {
|
|||||||
dateAdapter?: DateAdapter<Date>;
|
dateAdapter?: DateAdapter<Date>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit contract for `CalendarRoot`. The model events are 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 CalendarRootEmits {
|
export interface CalendarRootEmits {
|
||||||
'update:modelValue': [date: Date | Date[] | undefined];
|
'update:modelValue': [date: Date | Date[] | undefined];
|
||||||
'update:placeholder': [date: Date];
|
'update:placeholder': [date: Date];
|
||||||
@@ -106,8 +111,6 @@ const {
|
|||||||
dateAdapter,
|
dateAdapter,
|
||||||
} = defineProps<CalendarRootProps>();
|
} = defineProps<CalendarRootProps>();
|
||||||
|
|
||||||
defineEmits<CalendarRootEmits>();
|
|
||||||
|
|
||||||
defineSlots<{
|
defineSlots<{
|
||||||
default?: (props: {
|
default?: (props: {
|
||||||
date: Date;
|
date: Date;
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ export interface DatePickerRootProps extends PrimitiveProps,
|
|||||||
hourCycle?: HourCycle;
|
hourCycle?: HourCycle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit contract for `DatePickerRoot`. The model events are 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 DatePickerRootEmits {
|
export interface DatePickerRootEmits {
|
||||||
'update:modelValue': [date: Date | undefined];
|
'update:modelValue': [date: Date | undefined];
|
||||||
'update:placeholder': [date: Date];
|
'update:placeholder': [date: Date];
|
||||||
@@ -95,8 +100,6 @@ const {
|
|||||||
dateAdapter,
|
dateAdapter,
|
||||||
} = defineProps<DatePickerRootProps>();
|
} = defineProps<DatePickerRootProps>();
|
||||||
|
|
||||||
defineEmits<DatePickerRootEmits>();
|
|
||||||
|
|
||||||
const { forwardRef, currentElement: parentElement } = useForwardExpose();
|
const { forwardRef, currentElement: parentElement } = useForwardExpose();
|
||||||
|
|
||||||
// Resolve the effective date backend: per-instance prop wins over the global
|
// Resolve the effective date backend: per-instance prop wins over the global
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ export interface ProgressRootProps extends PrimitiveProps {
|
|||||||
accessibleLabel?: string | ((value: number | null, max: number) => string | undefined);
|
accessibleLabel?: string | ((value: number | null, max: number) => string | undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit contract for `ProgressRoot`. The model events are 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 ProgressRootEmits {
|
export interface ProgressRootEmits {
|
||||||
/** Emitted when the value changes (after validation/clamping). */
|
/** Emitted when the value changes (after validation/clamping). */
|
||||||
'update:modelValue': [value: number | null];
|
'update:modelValue': [value: number | null];
|
||||||
@@ -59,8 +64,6 @@ const {
|
|||||||
as = 'div',
|
as = 'div',
|
||||||
} = defineProps<ProgressRootProps>();
|
} = defineProps<ProgressRootProps>();
|
||||||
|
|
||||||
defineEmits<ProgressRootEmits>();
|
|
||||||
|
|
||||||
const { forwardRef } = useForwardExpose();
|
const { forwardRef } = useForwardExpose();
|
||||||
|
|
||||||
const localValue = ref<number | null>(null);
|
const localValue = ref<number | null>(null);
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ export interface SwitchProps<T = boolean> extends PrimitiveProps {
|
|||||||
value?: string;
|
value?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit contract for `Switch`. The model events are 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 SwitchEmits<T = boolean> {
|
export interface SwitchEmits<T = boolean> {
|
||||||
/** Emitted whenever the value changes (also drives `v-model`). */
|
/** Emitted whenever the value changes (also drives `v-model`). */
|
||||||
'update:modelValue': [value: T];
|
'update:modelValue': [value: T];
|
||||||
@@ -71,8 +76,6 @@ const {
|
|||||||
as = 'button',
|
as = 'button',
|
||||||
} = defineProps<SwitchProps<T>>();
|
} = defineProps<SwitchProps<T>>();
|
||||||
|
|
||||||
defineEmits<SwitchEmits<T>>();
|
|
||||||
|
|
||||||
const { forwardRef, currentElement } = useForwardExpose();
|
const { forwardRef, currentElement } = useForwardExpose();
|
||||||
|
|
||||||
const local = ref<T>((defaultValue ?? falsy) as T) as Ref<T>;
|
const local = ref<T>((defaultValue ?? falsy) as T) as Ref<T>;
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import type { PrimitiveProps } from '../../internal/primitive';
|
|||||||
/** Canonical `data-state` value reflected on the host element. */
|
/** Canonical `data-state` value reflected on the host element. */
|
||||||
export type ToggleState = 'on' | 'off';
|
export type ToggleState = 'on' | 'off';
|
||||||
|
|
||||||
/** Events emitted by `Toggle`. */
|
/**
|
||||||
|
* Emit contract for `Toggle`. The model events are 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 ToggleEmits {
|
export interface ToggleEmits {
|
||||||
/** Fired when the pressed state changes. Backs `v-model:pressed`. */
|
/** Fired when the pressed state changes. Backs `v-model:pressed`. */
|
||||||
'update:pressed': [pressed: boolean];
|
'update:pressed': [pressed: boolean];
|
||||||
@@ -58,8 +62,6 @@ const {
|
|||||||
value = 'on',
|
value = 'on',
|
||||||
} = defineProps<ToggleProps>();
|
} = defineProps<ToggleProps>();
|
||||||
|
|
||||||
defineEmits<ToggleEmits>();
|
|
||||||
|
|
||||||
const { forwardRef, currentElement } = useForwardExpose();
|
const { forwardRef, currentElement } = useForwardExpose();
|
||||||
|
|
||||||
// A standalone Toggle nested inside a ToggleGroup must not also submit its own
|
// A standalone Toggle nested inside a ToggleGroup must not also submit its own
|
||||||
|
|||||||
@@ -4,7 +4,35 @@ import { renderSlotChild } from './Slot';
|
|||||||
|
|
||||||
type FunctionalComponentContext = Omit<SetupContext, 'expose'>;
|
type FunctionalComponentContext = Omit<SetupContext, 'expose'>;
|
||||||
|
|
||||||
export interface PrimitiveProps {
|
type Booleanish = boolean | 'true' | 'false';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global DOM attributes any part accepts and forwards, through `$attrs`, to the
|
||||||
|
* element it renders. They are deliberately kept out of the runtime props (the
|
||||||
|
* `@vue-ignore` marker on the heritage clause below stops the SFC compiler from
|
||||||
|
* lifting them out of `$attrs`), so this only teaches `strictTemplates` that
|
||||||
|
* they are valid — the runtime behaviour is unchanged.
|
||||||
|
*/
|
||||||
|
export interface PrimitiveAttributes {
|
||||||
|
id?: string;
|
||||||
|
role?: string;
|
||||||
|
title?: string;
|
||||||
|
tabindex?: number | string;
|
||||||
|
lang?: string;
|
||||||
|
dir?: string;
|
||||||
|
hidden?: Booleanish | 'until-found' | '';
|
||||||
|
inert?: Booleanish;
|
||||||
|
autofocus?: Booleanish;
|
||||||
|
draggable?: Booleanish;
|
||||||
|
spellcheck?: Booleanish;
|
||||||
|
translate?: 'yes' | 'no';
|
||||||
|
nonce?: string;
|
||||||
|
part?: string;
|
||||||
|
slot?: string;
|
||||||
|
[key: `data-${string}` | `aria-${string}`]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrimitiveProps extends /* @vue-ignore */ PrimitiveAttributes {
|
||||||
as?: keyof IntrinsicElementAttributes | Component;
|
as?: keyof IntrinsicElementAttributes | Component;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export { Primitive, type PrimitiveProps } from './Primitive';
|
export { Primitive, type PrimitiveAttributes, type PrimitiveProps } from './Primitive';
|
||||||
export { Slot } from './Slot';
|
export { Slot } from './Slot';
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ export interface NavigationMenuRootProps extends PrimitiveProps {
|
|||||||
unmountOnHide?: boolean;
|
unmountOnHide?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit contract for `NavigationMenuRoot`. The model events are 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 NavigationMenuRootEmits {
|
export interface NavigationMenuRootEmits {
|
||||||
'update:modelValue': [value: string];
|
'update:modelValue': [value: string];
|
||||||
}
|
}
|
||||||
@@ -70,8 +75,6 @@ const {
|
|||||||
as = 'nav',
|
as = 'nav',
|
||||||
} = defineProps<NavigationMenuRootProps>();
|
} = defineProps<NavigationMenuRootProps>();
|
||||||
|
|
||||||
defineEmits<NavigationMenuRootEmits>();
|
|
||||||
|
|
||||||
defineSlots<{
|
defineSlots<{
|
||||||
default?: (props: { modelValue: string }) => unknown;
|
default?: (props: { modelValue: string }) => unknown;
|
||||||
}>();
|
}>();
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ export interface NavigationMenuSubProps extends PrimitiveProps {
|
|||||||
orientation?: Orientation;
|
orientation?: Orientation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit contract for `NavigationMenuSub`. The model events are 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 NavigationMenuSubEmits {
|
export interface NavigationMenuSubEmits {
|
||||||
'update:modelValue': [value: string];
|
'update:modelValue': [value: string];
|
||||||
}
|
}
|
||||||
@@ -35,8 +40,6 @@ defineOptions({ inheritAttrs: false });
|
|||||||
|
|
||||||
const { defaultValue, orientation = 'horizontal', as = 'div' } = defineProps<NavigationMenuSubProps>();
|
const { defaultValue, orientation = 'horizontal', as = 'div' } = defineProps<NavigationMenuSubProps>();
|
||||||
|
|
||||||
defineEmits<NavigationMenuSubEmits>();
|
|
||||||
|
|
||||||
defineSlots<{
|
defineSlots<{
|
||||||
default?: (props: { modelValue: string }) => unknown;
|
default?: (props: { modelValue: string }) => unknown;
|
||||||
}>();
|
}>();
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ export interface ToolbarRootEmits {
|
|||||||
/** Backs `v-model:currentTabStopId`. */
|
/** Backs `v-model:currentTabStopId`. */
|
||||||
'update:currentTabStopId': [value: string | null | undefined];
|
'update:currentTabStopId': [value: string | null | undefined];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The subset `defineEmits` declares. `update:currentTabStopId` comes from
|
||||||
|
* `defineModel`; passing a model key through `defineEmits` as well erases its
|
||||||
|
* payload type from the generated declarations, leaving consumers with
|
||||||
|
* `unknown`.
|
||||||
|
*/
|
||||||
|
type ToolbarRootOwnEmits = Omit<ToolbarRootEmits, 'update:currentTabStopId'>;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -64,7 +72,7 @@ const {
|
|||||||
as = 'div',
|
as = 'div',
|
||||||
} = defineProps<ToolbarRootProps>();
|
} = defineProps<ToolbarRootProps>();
|
||||||
|
|
||||||
const emit = defineEmits<ToolbarRootEmits>();
|
const emit = defineEmits<ToolbarRootOwnEmits>();
|
||||||
|
|
||||||
const { forwardRef } = useForwardExpose();
|
const { forwardRef } = useForwardExpose();
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ import { useSelectRootContext } from './context';
|
|||||||
import SelectContentImpl from './SelectContentImpl.vue';
|
import SelectContentImpl from './SelectContentImpl.vue';
|
||||||
import SelectProvider from './SelectProvider.vue';
|
import SelectProvider from './SelectProvider.vue';
|
||||||
|
|
||||||
|
// Neither branch below is a single element root (`Presence` wraps the panel,
|
||||||
|
// the closed branch is a `Teleport`), so Vue cannot inherit `class`/`style` or
|
||||||
|
// any other attribute automatically — they are forwarded onto the panel itself.
|
||||||
|
defineOptions({ inheritAttrs: false });
|
||||||
|
|
||||||
const props = defineProps<SelectContentProps>();
|
const props = defineProps<SelectContentProps>();
|
||||||
const emit = defineEmits<SelectContentEmits>();
|
const emit = defineEmits<SelectContentEmits>();
|
||||||
const rootCtx = useSelectRootContext();
|
const rootCtx = useSelectRootContext();
|
||||||
@@ -57,7 +62,7 @@ onMounted(() => {
|
|||||||
:present="present"
|
:present="present"
|
||||||
>
|
>
|
||||||
<SelectContentImpl
|
<SelectContentImpl
|
||||||
v-bind="props"
|
v-bind="{ ...props, ...$attrs }"
|
||||||
@close-auto-focus="emit('closeAutoFocus', $event)"
|
@close-auto-focus="emit('closeAutoFocus', $event)"
|
||||||
@escape-key-down="emit('escapeKeyDown', $event)"
|
@escape-key-down="emit('escapeKeyDown', $event)"
|
||||||
@pointer-down-outside="emit('pointerDownOutside', $event)"
|
@pointer-down-outside="emit('pointerDownOutside', $event)"
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
import type { Direction } from '../../utilities/config-provider';
|
import type { Direction } from '../../utilities/config-provider';
|
||||||
import type { AcceptableValue } from './utils';
|
import type { AcceptableValue } from './utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shape of the select's model value: an array of `T` in multiple mode, a bare
|
||||||
|
* `T` otherwise. Keeps `v-model` narrow on both sides of the binding.
|
||||||
|
*/
|
||||||
|
export type SelectModelValue<T extends AcceptableValue, Multiple extends boolean> = Multiple extends true ? T[] : T;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A custom, fully stylable replacement for the native `<select>` element: a
|
* A custom, fully stylable replacement for the native `<select>` element: a
|
||||||
* trigger button that opens a floating listbox of options, with full keyboard
|
* trigger button that opens a floating listbox of options, with full keyboard
|
||||||
@@ -16,7 +22,9 @@ import type { AcceptableValue } from './utils';
|
|||||||
* (compared via `by`). Compose it from a `SelectTrigger` (with
|
* (compared via `by`). Compose it from a `SelectTrigger` (with
|
||||||
* `SelectValue`/`SelectIcon`) plus a portalled `SelectContent` of `SelectItem`s.
|
* `SelectValue`/`SelectIcon`) plus a portalled `SelectContent` of `SelectItem`s.
|
||||||
*/
|
*/
|
||||||
export interface SelectRootProps<T extends AcceptableValue = AcceptableValue> {
|
export interface SelectRootProps<T extends AcceptableValue = AcceptableValue, Multiple extends boolean = false> {
|
||||||
|
/** Controlled value. Bind with `v-model`. */
|
||||||
|
modelValue?: SelectModelValue<T, Multiple>;
|
||||||
/** Reading direction. Falls back to ConfigProvider. */
|
/** Reading direction. Falls back to ConfigProvider. */
|
||||||
dir?: Direction;
|
dir?: Direction;
|
||||||
/** Disable the whole select. */
|
/** Disable the whole select. */
|
||||||
@@ -26,11 +34,11 @@ export interface SelectRootProps<T extends AcceptableValue = AcceptableValue> {
|
|||||||
/** Native input name for form submission. */
|
/** Native input name for form submission. */
|
||||||
name?: string;
|
name?: string;
|
||||||
/** Uncontrolled default value. */
|
/** Uncontrolled default value. */
|
||||||
defaultValue?: T | T[];
|
defaultValue?: SelectModelValue<T, Multiple>;
|
||||||
/** Uncontrolled default open state. */
|
/** Uncontrolled default open state. */
|
||||||
defaultOpen?: boolean;
|
defaultOpen?: boolean;
|
||||||
/** Allow selecting multiple options; the model becomes an array. */
|
/** Allow selecting multiple options; the model becomes an array. */
|
||||||
multiple?: boolean;
|
multiple?: Multiple;
|
||||||
/**
|
/**
|
||||||
* Compare object values by a property key or a custom comparator. Omitted →
|
* Compare object values by a property key or a custom comparator. Omitted →
|
||||||
* `===` for primitives / structural deep-equality for objects.
|
* `===` for primitives / structural deep-equality for objects.
|
||||||
@@ -40,13 +48,20 @@ export interface SelectRootProps<T extends AcceptableValue = AcceptableValue> {
|
|||||||
autocomplete?: string;
|
autocomplete?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectRootEmits<T extends AcceptableValue = AcceptableValue> {
|
export interface SelectRootEmits<T extends AcceptableValue = AcceptableValue, Multiple extends boolean = false> {
|
||||||
'update:modelValue': [value: T | T[] | undefined];
|
'update:modelValue': [value: SelectModelValue<T, Multiple>];
|
||||||
'update:open': [open: boolean];
|
'update:open': [open: boolean];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The subset `defineEmits` declares. `update:open` comes from `defineModel`;
|
||||||
|
* passing a model key through `defineEmits` as well erases its payload type
|
||||||
|
* from the generated declarations, leaving consumers with `unknown`.
|
||||||
|
*/
|
||||||
|
type SelectRootOwnEmits<T extends AcceptableValue, Multiple extends boolean> = Omit<SelectRootEmits<T, Multiple>, 'update:open'>;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts" generic="T extends AcceptableValue = AcceptableValue">
|
<script setup lang="ts" generic="T extends AcceptableValue = AcceptableValue, Multiple extends boolean = false">
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import { computed, ref, shallowRef, toRef, watch } from 'vue';
|
import { computed, ref, shallowRef, toRef, watch } from 'vue';
|
||||||
|
|
||||||
@@ -60,6 +75,7 @@ import { compare, shouldShowPlaceholder } from './utils';
|
|||||||
defineOptions({ inheritAttrs: false });
|
defineOptions({ inheritAttrs: false });
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
modelValue,
|
||||||
dir,
|
dir,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
required = false,
|
required = false,
|
||||||
@@ -69,11 +85,13 @@ const {
|
|||||||
multiple = false,
|
multiple = false,
|
||||||
by,
|
by,
|
||||||
autocomplete,
|
autocomplete,
|
||||||
} = defineProps<SelectRootProps<T>>();
|
} = defineProps<SelectRootProps<T, Multiple>>();
|
||||||
|
|
||||||
|
const emit = defineEmits<SelectRootOwnEmits<T, Multiple>>();
|
||||||
|
|
||||||
defineSlots<{
|
defineSlots<{
|
||||||
default?: (props: {
|
default?: (props: {
|
||||||
modelValue: T | T[] | undefined;
|
modelValue: SelectModelValue<T, Multiple> | undefined;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
}) => unknown;
|
}) => unknown;
|
||||||
}>();
|
}>();
|
||||||
@@ -88,16 +106,26 @@ const open = defineModel<boolean>('open', {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const localValue = ref<T | T[] | undefined>(defaultValue ?? (multiple ? ([] as T[]) : undefined)) as Ref<T | T[] | undefined>;
|
type ModelValue = SelectModelValue<T, Multiple>;
|
||||||
const value = defineModel<T | T[] | undefined>('modelValue', {
|
|
||||||
default: undefined,
|
// `defineModel` would type `update:modelValue` as `ModelValue | undefined`,
|
||||||
get: v => (v ?? localValue.value),
|
// forcing every consumer's `v-model` target to accept `undefined` even though
|
||||||
|
// a selection is never cleared. The prop and the emit are declared separately
|
||||||
|
// so the emitted payload stays exactly `ModelValue` (see AGENTS §3.2.3).
|
||||||
|
const localValue = ref(defaultValue ?? (multiple ? [] : undefined)) as Ref<ModelValue | undefined>;
|
||||||
|
const value = computed<ModelValue | undefined>({
|
||||||
|
get: () => modelValue ?? localValue.value,
|
||||||
set: (v) => {
|
set: (v) => {
|
||||||
localValue.value = v;
|
localValue.value = v;
|
||||||
return v;
|
emit('update:modelValue', v as ModelValue);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The public model type is conditional on `Multiple`, which TypeScript cannot
|
||||||
|
// narrow inside the component; the internal logic reads and writes the union
|
||||||
|
// through this widened alias instead.
|
||||||
|
const model = value as unknown as Ref<T | T[] | undefined>;
|
||||||
|
|
||||||
const contentId = useId(undefined, 'select-content');
|
const contentId = useId(undefined, 'select-content');
|
||||||
const dirRef = toRef(() => dir);
|
const dirRef = toRef(() => dir);
|
||||||
const disabledRef = toRef(() => disabled);
|
const disabledRef = toRef(() => disabled);
|
||||||
@@ -119,7 +147,7 @@ const displayValue = ref<string | undefined>(undefined);
|
|||||||
const rawOptions = new Set<SelectOption>();
|
const rawOptions = new Set<SelectOption>();
|
||||||
const optionsSet = shallowRef(new Set<SelectOption>());
|
const optionsSet = shallowRef(new Set<SelectOption>());
|
||||||
|
|
||||||
const isEmptyModelValue = computed(() => shouldShowPlaceholder(value.value));
|
const isEmptyModelValue = computed(() => shouldShowPlaceholder(model.value));
|
||||||
|
|
||||||
function getOptionFrom(source: Iterable<SelectOption>, v: AcceptableValue): SelectOption | undefined {
|
function getOptionFrom(source: Iterable<SelectOption>, v: AcceptableValue): SelectOption | undefined {
|
||||||
for (const option of source) {
|
for (const option of source) {
|
||||||
@@ -143,8 +171,8 @@ function onOptionRemove(option: SelectOption) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Persist a single-value label for the legacy `displayValue` slot path.
|
// Persist a single-value label for the legacy `displayValue` slot path.
|
||||||
watch([optionsSet, value], () => {
|
watch([optionsSet, model], () => {
|
||||||
const current = value.value;
|
const current = model.value;
|
||||||
if (current === undefined || Array.isArray(current)) return;
|
if (current === undefined || Array.isArray(current)) return;
|
||||||
const text = getOptionFrom(optionsSet.value, current)?.textContent;
|
const text = getOptionFrom(optionsSet.value, current)?.textContent;
|
||||||
if (text !== undefined) displayValue.value = text;
|
if (text !== undefined) displayValue.value = text;
|
||||||
@@ -152,21 +180,21 @@ watch([optionsSet, value], () => {
|
|||||||
|
|
||||||
function handleValueChange(newValue: AcceptableValue) {
|
function handleValueChange(newValue: AcceptableValue) {
|
||||||
if (multiple) {
|
if (multiple) {
|
||||||
const array = Array.isArray(value.value) ? [...value.value] : [];
|
const array = Array.isArray(model.value) ? [...model.value] : [];
|
||||||
const index = array.findIndex(v => compare(v as T, newValue as T, by as never));
|
const index = array.findIndex(v => compare(v as T, newValue as T, by as never));
|
||||||
if (index === -1) array.push(newValue as T);
|
if (index === -1) array.push(newValue as T);
|
||||||
else array.splice(index, 1);
|
else array.splice(index, 1);
|
||||||
value.value = [...array] as T[];
|
model.value = [...array] as T[];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
value.value = newValue as T;
|
model.value = newValue as T;
|
||||||
displayValue.value = getOptionFrom(rawOptions, newValue)?.textContent;
|
displayValue.value = getOptionFrom(rawOptions, newValue)?.textContent;
|
||||||
open.value = false;
|
open.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSelectedValue(itemValue: AcceptableValue): boolean {
|
function isSelectedValue(itemValue: AcceptableValue): boolean {
|
||||||
const current = value.value;
|
const current = model.value;
|
||||||
if (current === undefined) return false;
|
if (current === undefined) return false;
|
||||||
if (Array.isArray(current)) {
|
if (Array.isArray(current)) {
|
||||||
for (const v of current) {
|
for (const v of current) {
|
||||||
@@ -197,7 +225,7 @@ const isFormControl = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
provideSelectRootContext({
|
provideSelectRootContext({
|
||||||
value,
|
value: model,
|
||||||
onValueChange: handleValueChange,
|
onValueChange: handleValueChange,
|
||||||
open,
|
open,
|
||||||
onOpenChange: (v) => { open.value = v; },
|
onOpenChange: (v) => { open.value = v; },
|
||||||
@@ -237,7 +265,7 @@ provideSelectRootContext({
|
|||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:multiple="multiple"
|
:multiple="multiple"
|
||||||
:options="nativeOptions"
|
:options="nativeOptions"
|
||||||
:value="value"
|
:value="model"
|
||||||
@change="handleValueChange"
|
@change="handleValueChange"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -245,7 +273,7 @@ provideSelectRootContext({
|
|||||||
v-else-if="name"
|
v-else-if="name"
|
||||||
type="hidden"
|
type="hidden"
|
||||||
:name="name"
|
:name="name"
|
||||||
:value="Array.isArray(value) ? '' : (value ?? '')"
|
:value="Array.isArray(model) ? '' : (model ?? '')"
|
||||||
:required="required"
|
:required="required"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:autocomplete="autocomplete"
|
:autocomplete="autocomplete"
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ export interface SelectViewportProps extends PrimitiveProps {
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, toRef, watchPostEffect } from 'vue';
|
import { ref, toRef, watchPostEffect } from 'vue';
|
||||||
|
|
||||||
import { useForwardExpose } from '@robonen/vue';
|
import { useForwardExpose, useStyleTag } from '@robonen/vue';
|
||||||
import { useNonce } from '../../utilities/config-provider';
|
import { useNonce } from '../../utilities/config-provider';
|
||||||
import { Primitive } from '../../internal/primitive';
|
import { Primitive } from '../../internal/primitive';
|
||||||
import { useSelectContentContext, useSelectItemAlignedPositionContext } from './context';
|
import { useSelectContentContext, useSelectItemAlignedPositionContext } from './context';
|
||||||
import { CONTENT_MARGIN } from './utils';
|
import { CONTENT_MARGIN, VIEWPORT_SCROLLBAR_CSS } from './utils';
|
||||||
|
|
||||||
const { as = 'div', nonce: propNonce } = defineProps<SelectViewportProps>();
|
const { as = 'div', nonce: propNonce } = defineProps<SelectViewportProps>();
|
||||||
|
|
||||||
@@ -32,6 +32,11 @@ const { forwardRef, currentElement } = useForwardExpose();
|
|||||||
const contentCtx = useSelectContentContext();
|
const contentCtx = useSelectContentContext();
|
||||||
const nonce = useNonce(toRef(() => propNonce));
|
const nonce = useNonce(toRef(() => propNonce));
|
||||||
|
|
||||||
|
// Injected into `<head>` (one reference-counted tag per document) rather than
|
||||||
|
// rendered as a sibling `<style>`: a second root node would turn this component
|
||||||
|
// into a fragment, and Vue cannot inherit a consumer's `class` onto a fragment.
|
||||||
|
useStyleTag(VIEWPORT_SCROLLBAR_CSS, { id: 'primitives-select-viewport', nonce: nonce.value });
|
||||||
|
|
||||||
const alignedCtx = contentCtx.position === 'item-aligned'
|
const alignedCtx = contentCtx.position === 'item-aligned'
|
||||||
? useSelectItemAlignedPositionContext(null as never)
|
? useSelectItemAlignedPositionContext(null as never)
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -82,8 +87,4 @@ function handleScroll(event: Event) {
|
|||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</Primitive>
|
</Primitive>
|
||||||
<Primitive as="style" :nonce="nonce">
|
|
||||||
[data-primitives-select-viewport]{scrollbar-width:none;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;}
|
|
||||||
[data-primitives-select-viewport]::-webkit-scrollbar{display:none;}
|
|
||||||
</Primitive>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -385,3 +385,74 @@ describe('Select — native form submission', () => {
|
|||||||
w.unmount();
|
w.unmount();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Select — attribute forwarding on the panel', () => {
|
||||||
|
function mountStyled() {
|
||||||
|
return track(mount(
|
||||||
|
defineComponent({
|
||||||
|
setup() {
|
||||||
|
return () => h(
|
||||||
|
SelectRoot,
|
||||||
|
{ defaultOpen: true },
|
||||||
|
{
|
||||||
|
default: () => [
|
||||||
|
h(SelectTrigger, { id: 'styled-trigger', 'aria-label': 'Fruit' }, {
|
||||||
|
default: () => h(SelectValue, { placeholder: 'Pick one' }),
|
||||||
|
}),
|
||||||
|
h(SelectPortal, null, {
|
||||||
|
default: () => h(SelectContent, { class: 'panel', 'data-panel': 'yes' }, {
|
||||||
|
default: () => h(SelectViewport, { class: 'viewport' }, {
|
||||||
|
default: () => h(SelectItem, { value: 'apple' }, {
|
||||||
|
default: () => h(SelectItemText, null, { default: () => 'Apple' }),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ attachTo: document.body },
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
it('forwards class and data attributes from SelectContent to the panel element', async () => {
|
||||||
|
const w = mountStyled();
|
||||||
|
await flush();
|
||||||
|
const panel = document.querySelector('[data-primitives-select-content]') as HTMLElement | null;
|
||||||
|
expect(panel).toBeTruthy();
|
||||||
|
expect(panel!.classList.contains('panel')).toBe(true);
|
||||||
|
expect(panel!.getAttribute('data-panel')).toBe('yes');
|
||||||
|
w.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('forwards class from SelectViewport to the viewport element', async () => {
|
||||||
|
const w = mountStyled();
|
||||||
|
await flush();
|
||||||
|
const viewport = document.querySelector('[data-primitives-select-viewport]') as HTMLElement | null;
|
||||||
|
expect(viewport).toBeTruthy();
|
||||||
|
expect(viewport!.classList.contains('viewport')).toBe(true);
|
||||||
|
w.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the trigger a single root that accepts native attributes', async () => {
|
||||||
|
const w = mountStyled();
|
||||||
|
await flush();
|
||||||
|
const trigger = getTrigger();
|
||||||
|
expect(trigger.id).toBe('styled-trigger');
|
||||||
|
expect(trigger.getAttribute('aria-label')).toBe('Fruit');
|
||||||
|
w.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects the scrollbar-hiding stylesheet into head instead of a sibling style node', async () => {
|
||||||
|
const w = mountStyled();
|
||||||
|
await flush();
|
||||||
|
const injected = document.head.querySelector('#primitives-select-viewport');
|
||||||
|
expect(injected).toBeTruthy();
|
||||||
|
expect(injected!.textContent).toContain('[data-primitives-select-viewport]');
|
||||||
|
const panel = document.querySelector('[data-primitives-select-content]') as HTMLElement;
|
||||||
|
expect(panel.querySelector('style')).toBeNull();
|
||||||
|
w.unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -4,13 +4,6 @@ import type { AcceptableValue } from './utils';
|
|||||||
|
|
||||||
import { useContextFactory } from '@robonen/vue';
|
import { useContextFactory } from '@robonen/vue';
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Kept for backward compatibility. The select now accepts any
|
|
||||||
* {@link AcceptableValue} (string/number/boolean/object). `SelectValue` remains
|
|
||||||
* a string alias so existing `string`-typed consumers keep compiling.
|
|
||||||
*/
|
|
||||||
export type SelectValue = string;
|
|
||||||
|
|
||||||
export interface SelectOption {
|
export interface SelectOption {
|
||||||
value: AcceptableValue;
|
value: AcceptableValue;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ export {
|
|||||||
} from './context';
|
} from './context';
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
SelectValue,
|
|
||||||
SelectOption,
|
SelectOption,
|
||||||
SelectRootContext,
|
SelectRootContext,
|
||||||
SelectContentContext,
|
SelectContentContext,
|
||||||
@@ -38,7 +37,7 @@ export type {
|
|||||||
SelectItemContext,
|
SelectItemContext,
|
||||||
} from './context';
|
} from './context';
|
||||||
export type { AcceptableValue as SelectAcceptableValue } from './utils';
|
export type { AcceptableValue as SelectAcceptableValue } from './utils';
|
||||||
export type { SelectRootProps, SelectRootEmits } from './SelectRoot.vue';
|
export type { SelectModelValue, SelectRootProps, SelectRootEmits } from './SelectRoot.vue';
|
||||||
export type { SelectTriggerProps } from './SelectTrigger.vue';
|
export type { SelectTriggerProps } from './SelectTrigger.vue';
|
||||||
export type { SelectValueProps } from './SelectValue.vue';
|
export type { SelectValueProps } from './SelectValue.vue';
|
||||||
export type { SelectIconProps } from './SelectIcon.vue';
|
export type { SelectIconProps } from './SelectIcon.vue';
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ export const OPEN_KEYS = [' ', 'Enter', 'ArrowUp', 'ArrowDown'];
|
|||||||
export const SELECTION_KEYS = [' ', 'Enter'];
|
export const SELECTION_KEYS = [' ', 'Enter'];
|
||||||
export const CONTENT_MARGIN = 10;
|
export const CONTENT_MARGIN = 10;
|
||||||
|
|
||||||
|
/** Hides the viewport's scrollbar across engines while keeping it scrollable. */
|
||||||
|
export const VIEWPORT_SCROLLBAR_CSS
|
||||||
|
= '[data-primitives-select-viewport]{scrollbar-width:none;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;}'
|
||||||
|
+ '[data-primitives-select-viewport]::-webkit-scrollbar{display:none;}';
|
||||||
|
|
||||||
export function getOpenState(open: boolean): 'open' | 'closed' {
|
export function getOpenState(open: boolean): 'open' | 'closed' {
|
||||||
return open ? 'open' : 'closed';
|
return open ? 'open' : 'closed';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,14 @@ export interface RovingFocusGroupEmits {
|
|||||||
'update:currentTabStopId': [value: string | null | undefined];
|
'update:currentTabStopId': [value: string | null | undefined];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The subset `defineEmits` declares. `update:currentTabStopId` comes from
|
||||||
|
* `defineModel`; passing a model key through `defineEmits` as well erases its
|
||||||
|
* payload type from the generated declarations, leaving consumers with
|
||||||
|
* `unknown`.
|
||||||
|
*/
|
||||||
|
type RovingFocusGroupOwnEmits = Omit<RovingFocusGroupEmits, 'update:currentTabStopId'>;
|
||||||
|
|
||||||
export interface RovingFocusGroupContext {
|
export interface RovingFocusGroupContext {
|
||||||
orientation: Ref<Orientation | undefined>;
|
orientation: Ref<Orientation | undefined>;
|
||||||
dir: Ref<Direction>;
|
dir: Ref<Direction>;
|
||||||
@@ -77,7 +85,7 @@ const {
|
|||||||
as = 'div',
|
as = 'div',
|
||||||
} = defineProps<RovingFocusGroupProps>();
|
} = defineProps<RovingFocusGroupProps>();
|
||||||
|
|
||||||
const emit = defineEmits<RovingFocusGroupEmits>();
|
const emit = defineEmits<RovingFocusGroupOwnEmits>();
|
||||||
|
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
// `dir` falls back to the provider's configured direction when not given as prop.
|
// `dir` falls back to the provider's configured direction when not given as prop.
|
||||||
|
|||||||
Reference in New Issue
Block a user