mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-14 12:14:41 +01:00
fix(InputMenu/Select/SelectMenu): improve types (#2471)
This commit is contained in:
@@ -11,7 +11,7 @@ const fruits = ['Apple', 'Banana', 'Blueberry', 'Grapes', 'Pineapple']
|
||||
const vegetables = ['Aubergine', 'Broccoli', 'Carrot', 'Courgette', 'Leek']
|
||||
|
||||
const items = [[{ label: 'Fruits', type: 'label' }, ...fruits], [{ label: 'Vegetables', type: 'label' }, ...vegetables]]
|
||||
const selectedItems = ref([fruits[0], vegetables[0]])
|
||||
const selectedItems = ref([fruits[0]!, vegetables[0]!])
|
||||
|
||||
const statuses = [{
|
||||
label: 'Backlog',
|
||||
@@ -135,7 +135,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
v-for="size in sizes"
|
||||
:key="size"
|
||||
:items="items"
|
||||
:model-value="[fruits[0]]"
|
||||
:model-value="[fruits[0]!]"
|
||||
multiple
|
||||
icon="i-heroicons-magnifying-glass"
|
||||
placeholder="Search..."
|
||||
|
||||
@@ -11,7 +11,7 @@ const fruits = ['Apple', 'Banana', 'Blueberry', 'Grapes', 'Pineapple']
|
||||
const vegetables = ['Aubergine', 'Broccoli', 'Carrot', 'Courgette', 'Leek']
|
||||
|
||||
const items = [[{ label: 'Fruits', type: 'label' }, ...fruits], [{ label: 'Vegetables', type: 'label' }, ...vegetables]]
|
||||
const selectedItems = ref([fruits[0], vegetables[0]])
|
||||
const selectedItems = ref([fruits[0]!, vegetables[0]!])
|
||||
|
||||
const statuses = [{
|
||||
label: 'Backlog',
|
||||
|
||||
@@ -7,7 +7,7 @@ import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/input-menu'
|
||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||
import type { AvatarProps, ChipProps, InputProps } from '../types'
|
||||
import type { AcceptableValue, ArrayOrWrapped, PartialString } from '../types/utils'
|
||||
import type { AcceptableValue, ArrayOrWrapped, PartialString, SelectItems, SelectItemType, SelectModelValue, SelectModelValueEmits, SelectItemKey } from '../types/utils'
|
||||
|
||||
const appConfig = _appConfig as AppConfig & { ui: { inputMenu: Partial<typeof theme> } }
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface InputMenuItem {
|
||||
|
||||
type InputMenuVariants = VariantProps<typeof inputMenu>
|
||||
|
||||
export interface InputMenuProps<T> extends Pick<ComboboxRootProps<T>, 'modelValue' | 'defaultValue' | 'selectedValue' | 'open' | 'defaultOpen' | 'searchTerm' | 'multiple' | 'disabled' | 'name' | 'resetSearchTermOnBlur'>, UseComponentIconsProps {
|
||||
export interface InputMenuProps<T extends SelectItemType<I>, I extends SelectItems<InputMenuItem | AcceptableValue> = SelectItems<InputMenuItem | AcceptableValue>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false> extends Pick<ComboboxRootProps<T>, 'defaultValue' | 'selectedValue' | 'open' | 'defaultOpen' | 'searchTerm' | 'disabled' | 'name' | 'resetSearchTermOnBlur'>, UseComponentIconsProps {
|
||||
/**
|
||||
* The element or component this component should render as.
|
||||
* @defaultValue 'div'
|
||||
@@ -86,24 +86,28 @@ export interface InputMenuProps<T> extends Pick<ComboboxRootProps<T>, 'modelValu
|
||||
* When `items` is an array of objects, select the field to use as the value instead of the object itself.
|
||||
* @defaultValue undefined
|
||||
*/
|
||||
valueKey?: keyof T
|
||||
valueKey?: V
|
||||
/**
|
||||
* When `items` is an array of objects, select the field to use as the label.
|
||||
* @defaultValue 'label'
|
||||
*/
|
||||
labelKey?: keyof T
|
||||
items?: T[] | T[][]
|
||||
items?: I
|
||||
/** Highlight the ring color like a focus state. */
|
||||
highlight?: boolean
|
||||
class?: any
|
||||
ui?: PartialString<typeof inputMenu.slots>
|
||||
/** The controlled value of the Combobox. Can be binded-with with `v-model`. */
|
||||
modelValue?: SelectModelValue<T, V, M>
|
||||
/** Whether multiple options can be selected or not. */
|
||||
multiple?: M
|
||||
}
|
||||
|
||||
export type InputMenuEmits<T> = ComboboxRootEmits<T> & {
|
||||
export type InputMenuEmits<T, V, M extends boolean> = Omit<ComboboxRootEmits<T>, 'update:modelValue'> & {
|
||||
change: [payload: Event]
|
||||
blur: [payload: FocusEvent]
|
||||
focus: [payload: FocusEvent]
|
||||
}
|
||||
} & SelectModelValueEmits<T, V, M>
|
||||
|
||||
type SlotProps<T> = (props: { item: T, index: number }) => any
|
||||
|
||||
@@ -120,7 +124,7 @@ export interface InputMenuSlots<T> {
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends InputMenuItem | AcceptableValue">
|
||||
<script setup lang="ts" generic="T extends SelectItemType<I>, I extends SelectItems<InputMenuItem | AcceptableValue> = SelectItems<InputMenuItem | AcceptableValue>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false">
|
||||
import { computed, ref, toRef, onMounted } from 'vue'
|
||||
import { ComboboxRoot, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, TagsInputRoot, TagsInputItem, TagsInputItemText, TagsInputItemDelete, TagsInputInput, useForwardPropsEmits } from 'radix-vue'
|
||||
import { defu } from 'defu'
|
||||
@@ -137,14 +141,14 @@ import UChip from './Chip.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = withDefaults(defineProps<InputMenuProps<T>>(), {
|
||||
const props = withDefaults(defineProps<InputMenuProps<T, I, V, M>>(), {
|
||||
type: 'text',
|
||||
autofocusDelay: 0,
|
||||
portal: true,
|
||||
filter: () => ['label'],
|
||||
labelKey: 'label' as keyof T
|
||||
})
|
||||
const emits = defineEmits<InputMenuEmits<T>>()
|
||||
const emits = defineEmits<InputMenuEmits<T, V, M>>()
|
||||
const slots = defineSlots<InputMenuSlots<T>>()
|
||||
|
||||
const searchTerm = defineModel<string>('searchTerm', { default: '' })
|
||||
|
||||
@@ -6,7 +6,7 @@ import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/select'
|
||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||
import type { AvatarProps, ChipProps, InputProps } from '../types'
|
||||
import type { AcceptableValue, PartialString } from '../types/utils'
|
||||
import type { AcceptableValue, PartialString, SelectItems, SelectItemType, SelectModelValue, SelectModelValueEmits, SelectItemKey } from '../types/utils'
|
||||
|
||||
const appConfig = _appConfig as AppConfig & { ui: { select: Partial<typeof theme> } }
|
||||
|
||||
@@ -28,7 +28,7 @@ export interface SelectItem {
|
||||
|
||||
type SelectVariants = VariantProps<typeof select>
|
||||
|
||||
export interface SelectProps<T> extends Omit<SelectRootProps, 'dir'>, UseComponentIconsProps {
|
||||
export interface SelectProps<T extends SelectItemType<I>, I extends SelectItems<SelectItem | AcceptableValue> = SelectItems<SelectItem | AcceptableValue>, V extends SelectItemKey<T> | undefined = undefined> extends Omit<SelectRootProps, 'dir' | 'modelValue'>, UseComponentIconsProps {
|
||||
id?: string
|
||||
/** The placeholder text when the select is empty. */
|
||||
placeholder?: string
|
||||
@@ -64,24 +64,26 @@ export interface SelectProps<T> extends Omit<SelectRootProps, 'dir'>, UseCompone
|
||||
* When `items` is an array of objects, select the field to use as the value.
|
||||
* @defaultValue 'value'
|
||||
*/
|
||||
valueKey?: string
|
||||
valueKey?: V
|
||||
/**
|
||||
* When `items` is an array of objects, select the field to use as the label.
|
||||
* @defaultValue 'label'
|
||||
*/
|
||||
labelKey?: string
|
||||
items?: T[] | T[][]
|
||||
labelKey?: SelectItemKey<T>
|
||||
items?: I
|
||||
/** Highlight the ring color like a focus state. */
|
||||
highlight?: boolean
|
||||
class?: any
|
||||
ui?: PartialString<typeof select.slots>
|
||||
/** The controlled value of the Select. Can be bind as `v-model`. */
|
||||
modelValue?: SelectModelValue<T, V, false, T extends { value: infer U } ? U : never>
|
||||
}
|
||||
|
||||
export type SelectEmits = SelectRootEmits & {
|
||||
export type SelectEmits<T, V> = Omit<SelectRootEmits, 'update:modelValue'> & {
|
||||
change: [payload: Event]
|
||||
blur: [payload: FocusEvent]
|
||||
focus: [payload: FocusEvent]
|
||||
}
|
||||
} & SelectModelValueEmits<T, V, false, T extends { value: infer U } ? U : never>
|
||||
|
||||
type SlotProps<T> = (props: { item: T, index: number }) => any
|
||||
|
||||
@@ -95,7 +97,7 @@ export interface SelectSlots<T> {
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends SelectItem | AcceptableValue">
|
||||
<script setup lang="ts" generic="T extends SelectItemType<I>, I extends SelectItems<SelectItem | AcceptableValue> = SelectItems<SelectItem | AcceptableValue>, V extends SelectItemKey<T> | undefined = undefined">
|
||||
import { computed, toRef } from 'vue'
|
||||
import { SelectRoot, SelectTrigger, SelectValue, SelectPortal, SelectContent, SelectViewport, SelectLabel, SelectGroup, SelectItem, SelectItemIndicator, SelectItemText, SelectSeparator, useForwardPropsEmits } from 'radix-vue'
|
||||
import { defu } from 'defu'
|
||||
@@ -109,12 +111,12 @@ import UIcon from './Icon.vue'
|
||||
import UAvatar from './Avatar.vue'
|
||||
import UChip from './Chip.vue'
|
||||
|
||||
const props = withDefaults(defineProps<SelectProps<T>>(), {
|
||||
valueKey: 'value',
|
||||
labelKey: 'label',
|
||||
const props = withDefaults(defineProps<SelectProps<T, I, V>>(), {
|
||||
valueKey: 'value' as never,
|
||||
labelKey: 'label' as never,
|
||||
portal: true
|
||||
})
|
||||
const emits = defineEmits<SelectEmits>()
|
||||
const emits = defineEmits<SelectEmits<T, V>>()
|
||||
const slots = defineSlots<SelectSlots<T>>()
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
@@ -166,6 +168,9 @@ function onUpdateOpen(value: boolean) {
|
||||
v-slot="{ modelValue, open }"
|
||||
v-bind="rootProps"
|
||||
:name="name"
|
||||
:default-value="(defaultValue as string)"
|
||||
:model-value="(modelValue as string)"
|
||||
:autocomplete="autocomplete"
|
||||
:disabled="disabled"
|
||||
@update:model-value="onUpdate"
|
||||
@update:open="onUpdateOpen"
|
||||
|
||||
@@ -6,7 +6,7 @@ import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/select-menu'
|
||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||
import type { AvatarProps, ChipProps, InputProps } from '../types'
|
||||
import type { AcceptableValue, ArrayOrWrapped, PartialString } from '../types/utils'
|
||||
import type { AcceptableValue, ArrayOrWrapped, PartialString, SelectItems, SelectItemType, SelectModelValue, SelectModelValueEmits, SelectItemKey } from '../types/utils'
|
||||
|
||||
const appConfig = _appConfig as AppConfig & { ui: { selectMenu: Partial<typeof theme> } }
|
||||
|
||||
@@ -28,7 +28,7 @@ export interface SelectMenuItem {
|
||||
|
||||
type SelectMenuVariants = VariantProps<typeof selectMenu>
|
||||
|
||||
export interface SelectMenuProps<T> extends Pick<ComboboxRootProps<T>, 'modelValue' | 'defaultValue' | 'selectedValue' | 'open' | 'defaultOpen' | 'searchTerm' | 'multiple' | 'disabled' | 'name' | 'resetSearchTermOnBlur'>, UseComponentIconsProps {
|
||||
export interface SelectMenuProps<T extends SelectItemType<I>, I extends SelectItems<SelectMenuItem | AcceptableValue> = SelectItems<SelectMenuItem | AcceptableValue>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false> extends Pick<ComboboxRootProps<T>, 'defaultValue' | 'selectedValue' | 'open' | 'defaultOpen' | 'searchTerm' | 'disabled' | 'name' | 'resetSearchTermOnBlur'>, UseComponentIconsProps {
|
||||
id?: string
|
||||
/** The placeholder text when the select is empty. */
|
||||
placeholder?: string
|
||||
@@ -77,24 +77,28 @@ export interface SelectMenuProps<T> extends Pick<ComboboxRootProps<T>, 'modelVal
|
||||
* When `items` is an array of objects, select the field to use as the value instead of the object itself.
|
||||
* @defaultValue undefined
|
||||
*/
|
||||
valueKey?: keyof T
|
||||
valueKey?: V
|
||||
/**
|
||||
* When `items` is an array of objects, select the field to use as the label.
|
||||
* @defaultValue 'label'
|
||||
*/
|
||||
labelKey?: keyof T
|
||||
items?: T[] | T[][]
|
||||
labelKey?: SelectItemKey<T>
|
||||
items?: I
|
||||
/** Highlight the ring color like a focus state. */
|
||||
highlight?: boolean
|
||||
class?: any
|
||||
ui?: PartialString<typeof selectMenu.slots>
|
||||
/** The controlled value of the Combobox. Can be binded-with with `v-model`. */
|
||||
modelValue?: SelectModelValue<T, V, M>
|
||||
/** Whether multiple options can be selected or not. */
|
||||
multiple?: M
|
||||
}
|
||||
|
||||
export type SelectMenuEmits<T> = ComboboxRootEmits<T> & {
|
||||
export type SelectMenuEmits<T, V, M extends boolean> = Omit<ComboboxRootEmits<T>, 'update:modelValue'> & {
|
||||
change: [payload: Event]
|
||||
blur: [payload: FocusEvent]
|
||||
focus: [payload: FocusEvent]
|
||||
}
|
||||
} & SelectModelValueEmits<T, V, M>
|
||||
|
||||
type SlotProps<T> = (props: { item: T, index: number }) => any
|
||||
|
||||
@@ -110,7 +114,7 @@ export interface SelectMenuSlots<T> {
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends SelectMenuItem | AcceptableValue">
|
||||
<script setup lang="ts" generic="T extends SelectItemType<I>, I extends SelectItems<SelectMenuItem | AcceptableValue> = SelectItems<SelectMenuItem | AcceptableValue>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false">
|
||||
import { computed, toRef } from 'vue'
|
||||
import { ComboboxRoot, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, useForwardPropsEmits } from 'radix-vue'
|
||||
import { defu } from 'defu'
|
||||
@@ -125,15 +129,16 @@ import UIcon from './Icon.vue'
|
||||
import UAvatar from './Avatar.vue'
|
||||
import UChip from './Chip.vue'
|
||||
|
||||
const props = withDefaults(defineProps<SelectMenuProps<T>>(), {
|
||||
const props = withDefaults(defineProps<SelectMenuProps<T, I, V, M>>(), {
|
||||
search: true,
|
||||
portal: true,
|
||||
autofocusDelay: 0,
|
||||
searchInput: () => ({ placeholder: 'Search...' }),
|
||||
filter: () => ['label'],
|
||||
labelKey: 'label' as keyof T
|
||||
labelKey: 'label' as never
|
||||
})
|
||||
const emits = defineEmits<SelectMenuEmits<T>>()
|
||||
|
||||
const emits = defineEmits<SelectMenuEmits<T, V, M>>()
|
||||
const slots = defineSlots<SelectMenuSlots<T>>()
|
||||
|
||||
const searchTerm = defineModel<string>('searchTerm', { default: '' })
|
||||
@@ -158,7 +163,7 @@ const ui = computed(() => selectMenu({
|
||||
buttonGroup: orientation.value
|
||||
}))
|
||||
|
||||
function displayValue(value: T): string {
|
||||
function displayValue(value: T | T[]): string {
|
||||
if (props.multiple && Array.isArray(value)) {
|
||||
return value.map(v => displayValue(v)).join(', ')
|
||||
}
|
||||
@@ -168,7 +173,7 @@ function displayValue(value: T): string {
|
||||
return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
|
||||
}
|
||||
|
||||
function filterFunction(items: ArrayOrWrapped<AcceptableValue>, searchTerm: string): ArrayOrWrapped<AcceptableValue> {
|
||||
function filterFunction(items: ArrayOrWrapped<T>, searchTerm: string): ArrayOrWrapped<T> {
|
||||
if (props.filter === false) {
|
||||
return items
|
||||
}
|
||||
@@ -176,7 +181,7 @@ function filterFunction(items: ArrayOrWrapped<AcceptableValue>, searchTerm: stri
|
||||
const fields = Array.isArray(props.filter) ? props.filter : [props.labelKey]
|
||||
const escapedSearchTerm = escapeRegExp(searchTerm)
|
||||
|
||||
return items.filter((item) => {
|
||||
return items.filter((item: T) => {
|
||||
if (typeof item !== 'object') {
|
||||
return String(item).search(new RegExp(escapedSearchTerm, 'i')) !== -1
|
||||
}
|
||||
|
||||
@@ -24,3 +24,11 @@ export type ArrayOrWrapped<T> = T extends any[] ? T : Array<T>
|
||||
export type PartialString<T> = {
|
||||
[K in keyof T]?: string
|
||||
}
|
||||
|
||||
export type SelectItems<T> = T[] | T[][]
|
||||
export type SelectItemType<I extends SelectItems<unknown>> = I extends (infer U)[][] ? U : I extends (infer U)[] ? U : never
|
||||
export type SelectModelValue<T, V, M extends boolean = false, DV = T> = (T extends Record<string, any> ? V extends keyof T ? T[V] : DV : T) extends infer U ? M extends true ? U[] : U : never
|
||||
export type SelectItemKey<T> = (T extends Record<string, any> ? keyof T : string)
|
||||
export type SelectModelValueEmits<T, V, M extends boolean = false, DV = T> = {
|
||||
'update:modelValue': [payload: SelectModelValue<T, V, M, DV>]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import theme from '#build/ui/input'
|
||||
import { renderForm } from '../utils/form'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import type { FormInputEvents } from '~/src/module'
|
||||
import { expectEmitPayloadType } from '../utils/types'
|
||||
|
||||
describe('InputMenu', () => {
|
||||
const sizes = Object.keys(theme.variants.size) as any
|
||||
@@ -157,5 +158,66 @@ describe('InputMenu', () => {
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).not.toContain('Error message')
|
||||
})
|
||||
|
||||
test('should have the correct types', () => {
|
||||
// with object item
|
||||
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
||||
items: [{ label: 'foo', value: 'bar' }]
|
||||
})).toEqualTypeOf<[{ label: string, value: string }]>()
|
||||
|
||||
// with object item and multiple
|
||||
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
||||
items: [{ label: 'foo', value: 1 }],
|
||||
multiple: true
|
||||
})).toEqualTypeOf<[{ label: string, value: number }[]]>()
|
||||
|
||||
// with object item and valueKey
|
||||
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
||||
items: [{ label: 'foo', value: 'bar' }],
|
||||
valueKey: 'value'
|
||||
})).toEqualTypeOf<[string]>()
|
||||
|
||||
// with object item and multiple and valueKey
|
||||
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
||||
items: [{ label: 'foo', value: 1 }],
|
||||
multiple: true,
|
||||
valueKey: 'value'
|
||||
})).toEqualTypeOf<[number[]]>()
|
||||
|
||||
// with string item
|
||||
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
||||
items: ['foo']
|
||||
})).toEqualTypeOf<[string]>()
|
||||
|
||||
// with string item and multiple
|
||||
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
||||
items: ['foo'],
|
||||
multiple: true
|
||||
})).toEqualTypeOf<[string[]]>()
|
||||
|
||||
// with groups
|
||||
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
||||
items: [['foo']]
|
||||
})).toEqualTypeOf<[string]>()
|
||||
|
||||
// with groups and multiple
|
||||
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
||||
items: [['foo']],
|
||||
multiple: true
|
||||
})).toEqualTypeOf<[string[]]>()
|
||||
|
||||
// with groups, multiple and mixed types
|
||||
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
||||
items: [['foo', { value: 1 }], [{ value: 'bar' }, 2]],
|
||||
multiple: true
|
||||
})).toEqualTypeOf<[(string | number | { value: string } | { value: number })[]]>()
|
||||
|
||||
// with groups, multiple, mixed types and valueKey
|
||||
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
||||
items: [['foo', { value: 1 }], [{ value: 'bar' }, 2]],
|
||||
multiple: true,
|
||||
valueKey: 'value'
|
||||
})).toEqualTypeOf<[(string | number)[]]>()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import ComponentRender from '../component-render'
|
||||
import theme from '#build/ui/input'
|
||||
import { renderForm } from '../utils/form'
|
||||
import type { FormInputEvents } from '~/src/module'
|
||||
import { expectEmitPayloadType } from '../utils/types'
|
||||
|
||||
describe('Select', () => {
|
||||
const sizes = Object.keys(theme.variants.size) as any
|
||||
@@ -166,5 +167,33 @@ describe('Select', () => {
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).not.toContain('Error message')
|
||||
})
|
||||
|
||||
test('should have the correct types', () => {
|
||||
// with object item
|
||||
expectEmitPayloadType('update:modelValue', () => Select({
|
||||
items: [{ label: 'foo', value: 'bar' }]
|
||||
})).toEqualTypeOf<[string]>()
|
||||
|
||||
// with string item
|
||||
expectEmitPayloadType('update:modelValue', () => Select({
|
||||
items: ['foo']
|
||||
})).toEqualTypeOf<[string]>()
|
||||
|
||||
// with groups
|
||||
expectEmitPayloadType('update:modelValue', () => Select({
|
||||
items: [['foo']]
|
||||
})).toEqualTypeOf<[string]>()
|
||||
|
||||
// with groups and mixed types
|
||||
expectEmitPayloadType('update:modelValue', () => Select({
|
||||
items: [['foo', { value: 1 }], [{ value: 'bar' }, 2]]
|
||||
})).toEqualTypeOf<[string | number]>()
|
||||
|
||||
// with groups, mixed types and valueKey = undefined
|
||||
expectEmitPayloadType('update:modelValue', () => Select({
|
||||
items: [['foo', { value: 1 }], [{ value: 'bar' }, 2]],
|
||||
valueKey: undefined
|
||||
})).toEqualTypeOf<[string | number]>()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import theme from '#build/ui/input'
|
||||
import { renderForm } from '../utils/form'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import type { FormInputEvents } from '~/src/module'
|
||||
import { expectEmitPayloadType } from '../utils/types'
|
||||
|
||||
describe('SelectMenu', () => {
|
||||
const sizes = Object.keys(theme.variants.size) as any
|
||||
@@ -161,5 +162,66 @@ describe('SelectMenu', () => {
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).not.toContain('Error message')
|
||||
})
|
||||
|
||||
test('should have the correct types', () => {
|
||||
// with object item
|
||||
expectEmitPayloadType('update:modelValue', () => SelectMenu({
|
||||
items: [{ label: 'foo', value: 'bar' }]
|
||||
})).toEqualTypeOf<[{ label: string, value: string }]>()
|
||||
|
||||
// with object item and multiple
|
||||
expectEmitPayloadType('update:modelValue', () => SelectMenu({
|
||||
items: [{ label: 'foo', value: 1 }],
|
||||
multiple: true
|
||||
})).toEqualTypeOf<[{ label: string, value: number }[]]>()
|
||||
|
||||
// with object item and valueKey
|
||||
expectEmitPayloadType('update:modelValue', () => SelectMenu({
|
||||
items: [{ label: 'foo', value: 'bar' }],
|
||||
valueKey: 'value'
|
||||
})).toEqualTypeOf<[string]>()
|
||||
|
||||
// with object item and multiple and valueKey
|
||||
expectEmitPayloadType('update:modelValue', () => SelectMenu({
|
||||
items: [{ label: 'foo', value: 1 }],
|
||||
multiple: true,
|
||||
valueKey: 'value'
|
||||
})).toEqualTypeOf<[number[]]>()
|
||||
|
||||
// with string item
|
||||
expectEmitPayloadType('update:modelValue', () => SelectMenu({
|
||||
items: ['foo']
|
||||
})).toEqualTypeOf<[string]>()
|
||||
|
||||
// with string item and multiple
|
||||
expectEmitPayloadType('update:modelValue', () => SelectMenu({
|
||||
items: ['foo'],
|
||||
multiple: true
|
||||
})).toEqualTypeOf<[string[]]>()
|
||||
|
||||
// with groups
|
||||
expectEmitPayloadType('update:modelValue', () => SelectMenu({
|
||||
items: [['foo']]
|
||||
})).toEqualTypeOf<[string]>()
|
||||
|
||||
// with groups and multiple
|
||||
expectEmitPayloadType('update:modelValue', () => SelectMenu({
|
||||
items: [['foo']],
|
||||
multiple: true
|
||||
})).toEqualTypeOf<[string[]]>()
|
||||
|
||||
// with groups, multiple and mixed types
|
||||
expectEmitPayloadType('update:modelValue', () => SelectMenu({
|
||||
items: [['foo', { value: 1 }], [{ value: 'bar' }, 2]],
|
||||
multiple: true
|
||||
})).toEqualTypeOf<[(string | number | { value: string } | { value: number })[]]>()
|
||||
|
||||
// with groups, multiple, mixed types and valueKey
|
||||
expectEmitPayloadType('update:modelValue', () => SelectMenu({
|
||||
items: [['foo', { value: 1 }], [{ value: 'bar' }, 2]],
|
||||
multiple: true,
|
||||
valueKey: 'value'
|
||||
})).toEqualTypeOf<[(string | number)[]]>()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
13
test/utils/types.ts
Normal file
13
test/utils/types.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { expectTypeOf } from 'vitest'
|
||||
import type { VNode } from 'vue'
|
||||
|
||||
/**
|
||||
* Expect the type of a component emit payload.
|
||||
*/
|
||||
export function expectEmitPayloadType<T extends VNode, E extends keyof Events<T>>(_event: E, _cb: () => T) {
|
||||
return expectTypeOf<NonNullable<Events<T>[E]>>()
|
||||
}
|
||||
|
||||
type Events<T> = T extends { __ctx?: { props: infer Props } } ? {
|
||||
[K in keyof Props as K extends `on${infer E}${infer Rest}` ? `${Lowercase<E>}${Rest}` : never]: NonNullable<Props[K]> extends (...args: infer P) => any ? P : never
|
||||
} : never
|
||||
Reference in New Issue
Block a user