fix(InputMenu/Select/SelectMenu): improve types (#2471)

This commit is contained in:
Yasser Lahbibi
2024-10-28 18:08:24 +01:00
committed by GitHub
parent 1402436c2b
commit db8111d783
10 changed files with 226 additions and 38 deletions

View File

@@ -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: '' })

View File

@@ -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"

View File

@@ -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
}