feat(SelectMenu): handle multiple prop

Resolves #102
This commit is contained in:
Benjamin Canac
2024-05-13 15:54:50 +02:00
parent 7a376b5e49
commit 27ffb8d8ab
8 changed files with 698 additions and 542 deletions

View File

@@ -38,7 +38,7 @@ const searchTermDebounced = refDebounced(searchTerm, 200)
const { data: users, pending } = await useFetch('https://jsonplaceholder.typicode.com/users', {
params: { q: searchTermDebounced },
transform: (data: User[]) => {
return data?.map(user => ({ label: user.name, value: user.id, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` } })) || []
return data?.map(user => ({ id: user.id, label: user.name, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` } })) || []
},
lazy: true
})
@@ -53,6 +53,7 @@ const { data: users, pending } = await useFetch('https://jsonplaceholder.typicod
<USelectMenu :items="items" placeholder="Search..." variant="none" />
<USelectMenu :items="items" placeholder="Disabled" disabled />
<USelectMenu :items="items" placeholder="Required" required />
<USelectMenu :items="items" placeholder="Multiple" multiple />
<USelectMenu :items="items" loading placeholder="Search..." />
<USelectMenu :items="items" loading leading-icon="i-heroicons-magnifying-glass" placeholder="Search..." />
<USelectMenu :items="statuses" placeholder="Search status..." icon="i-heroicons-magnifying-glass" trailing-icon="i-heroicons-chevron-up-down-20-solid">
@@ -67,6 +68,7 @@ const { data: users, pending } = await useFetch('https://jsonplaceholder.typicod
:filter="false"
icon="i-heroicons-user"
placeholder="Search users..."
@update:open="searchTerm = ''"
>
<template #leading="{ modelValue }">
<UAvatar v-if="modelValue?.avatar" size="2xs" v-bind="modelValue.avatar" />

View File

@@ -26,7 +26,7 @@ export interface SelectMenuItem extends Pick<ComboboxItemProps, 'disabled'> {
type SelectMenuVariants = VariantProps<typeof selectMenu>
export interface SelectMenuProps<T> extends Omit<ComboboxRootProps<T>, 'asChild' | 'dir' | 'filterFunction' | 'displayValue' | 'multiple'>, UseComponentIconsProps {
export interface SelectMenuProps<T> extends Omit<ComboboxRootProps<T>, 'asChild' | 'dir' | 'filterFunction' | 'displayValue'>, UseComponentIconsProps {
id?: string
/** The placeholder text when the select is empty. */
placeholder?: string
@@ -97,7 +97,7 @@ const slots = defineSlots<SelectMenuSlots<T>>()
const searchTerm = defineModel<string>('searchTerm', { default: '' })
const appConfig = useAppConfig()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'open', 'defaultOpen'), emits)
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'open', 'defaultOpen', 'multiple'), emits)
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, position: 'popper' }) as ComboboxContentProps)
const { emitFormBlur, emitFormChange, size: formGroupSize, color, id, name, disabled } = useFormField<InputProps>(props)
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
@@ -115,7 +115,11 @@ const ui = computed(() => tv({ extend: selectMenu, slots: props.ui })({
buttonGroup: orientation.value
}))
function displayValue(val: AcceptableValue) {
function displayValue(val: T, multiple?: boolean): string {
if (multiple && Array.isArray(val)) {
return val.map(v => displayValue(v)).join(', ')
}
if (typeof val === 'object') {
return val.label
}
@@ -155,7 +159,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
as-child
:name="name"
:disabled="disabled"
:display-value="displayValue"
:display-value="() => searchTerm"
:filter-function="filterFunction"
@update:model-value="emitFormChange()"
>
@@ -168,8 +172,8 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
</span>
<slot :model-value="(modelValue as T)" :open="open">
<span v-if="displayValue(modelValue)" :class="ui.value()">
{{ displayValue(modelValue) }}
<span v-if="multiple ? !!modelValue?.length : !!modelValue" :class="ui.value()">
{{ displayValue(modelValue as T, multiple) }}
</span>
<span v-else :class="ui.placeholder()">
{{ placeholder ?? '&nbsp;' }}

View File

@@ -14,7 +14,7 @@ describe('Input', () => {
['with id', { props: { id: 'id' } }],
['with name', { props: { name: 'name' } }],
['with type', { props: { type: 'password' } }],
['with placeholder', { props: { placeholder: 'Enter your username' } }],
['with placeholder', { props: { placeholder: 'Search...' } }],
['with disabled', { props: { disabled: true } }],
['with required', { props: { required: true } }],
['with icon', { props: { icon: 'i-heroicons-magnifying-glass' } }],

View File

@@ -39,7 +39,7 @@ describe('Select', () => {
['with defaultValue', { props: { ...props, defaultValue: items[0] } }],
['with id', { props: { ...props, id: 'id' } }],
['with name', { props: { ...props, name: 'name' } }],
['with placeholder', { props: { ...props, placeholder: 'Enter your username' } }],
['with placeholder', { props: { ...props, placeholder: 'Search...' } }],
['with disabled', { props: { ...props, disabled: true } }],
['with required', { props: { ...props, required: true } }],
['with icon', { props: { ...props, icon: 'i-heroicons-magnifying-glass' } }],

View File

@@ -37,9 +37,12 @@ describe('SelectMenu', () => {
['with items', { props }],
['with modelValue', { props: { ...props, modelValue: items[0] } }],
['with defaultValue', { props: { ...props, defaultValue: items[0] } }],
['with multiple', { props: { ...props, multiple: true } }],
['with multiple and modelValue', { props: { ...props, multiple: true, modelValue: [items[0], items[1]] } }],
['with id', { props: { ...props, id: 'id' } }],
['with name', { props: { ...props, name: 'name' } }],
['with placeholder', { props: { ...props, placeholder: 'Enter your username' } }],
['with placeholder', { props: { ...props, placeholder: 'Search...' } }],
['with searchPlaceholder', { props: { ...props, searchPlaceholder: 'Filter items...' } }],
['with disabled', { props: { ...props, disabled: true } }],
['with required', { props: { ...props, required: true } }],
['with icon', { props: { ...props, icon: 'i-heroicons-magnifying-glass' } }],

View File

@@ -107,7 +107,7 @@ exports[`Input > renders with name correctly 1`] = `
`;
exports[`Input > renders with placeholder correctly 1`] = `
"<div class="relative inline-flex items-center"><input type="text" placeholder="Enter your username" class="w-full rounded-md border-0 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 px-2.5 py-1.5 text-sm shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring ring-inset ring-gray-300 dark:ring-gray-700 focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400">
"<div class="relative inline-flex items-center"><input type="text" placeholder="Search..." class="w-full rounded-md border-0 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 px-2.5 py-1.5 text-sm shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring ring-inset ring-gray-300 dark:ring-gray-700 focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400">
<!--v-if-->
<!--v-if-->
</div>"

View File

@@ -1287,7 +1287,7 @@ exports[`Select > renders with name correctly 1`] = `
exports[`Select > renders with placeholder correctly 1`] = `
"<button role="combobox" type="button" aria-controls="radix-vue-select-content-36" aria-expanded="true" aria-required="false" aria-autocomplete="none" dir="ltr" data-state="open" data-placeholder="" class="relative group rounded-md inline-flex items-center focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 px-2.5 py-1.5 text-sm shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring ring-inset ring-gray-300 dark:ring-gray-700 focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 pr-9">
<!--v-if--><span style="pointer-events: none;" class="truncate group-data-placeholder:text-current/50">Enter your username</span><span class="absolute inset-y-0 end-0 flex items-center pr-2.5"><span class="iconify i-heroicons:chevron-down-20-solid shrink-0 text-gray-400 dark:text-gray-500 size-5" aria-hidden="true"></span></span>
<!--v-if--><span style="pointer-events: none;" class="truncate group-data-placeholder:text-current/50">Search...</span><span class="absolute inset-y-0 end-0 flex items-center pr-2.5"><span class="iconify i-heroicons:chevron-down-20-solid shrink-0 text-gray-400 dark:text-gray-500 size-5" aria-hidden="true"></span></span>
</button>
<!--teleport start-->

File diff suppressed because it is too large Load Diff