feat(SelectMenu): add clearble

This commit is contained in:
rdjanuar
2024-11-12 18:47:39 +07:00
parent 3a5960fb58
commit 5a414eb55a
6 changed files with 182 additions and 37 deletions

View File

@@ -0,0 +1,102 @@
<script setup lang="ts">
const options = ref([
{ id: 1, name: 'bug', color: 'd73a4a' },
{ id: 2, name: 'documentation', color: '0075ca' },
{ id: 3, name: 'duplicate', color: 'cfd3d7' },
{ id: 4, name: 'enhancement', color: 'a2eeef' },
{ id: 5, name: 'good first issue', color: '7057ff' },
{ id: 6, name: 'help wanted', color: '008672' },
{ id: 7, name: 'invalid', color: 'e4e669' },
{ id: 8, name: 'question', color: 'd876e3' },
{ id: 9, name: 'wontfix', color: 'ffffff' }
])
const selected = ref([])
const labels = computed({
get: () => selected.value,
set: async (labels) => {
const promises = labels.map(async (label) => {
if (label.id) {
return label
}
// In a real app, you would make an API call to create the label
const response = {
id: options.value.length + 1,
name: label.name,
color: generateColorFromString(label.name)
}
options.value.push(response)
return response
})
selected.value = await Promise.all(promises)
}
})
function hashCode(str) {
let hash = 0
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash)
}
return hash
}
function intToRGB(i) {
const c = (i & 0x00FFFFFF)
.toString(16)
.toUpperCase()
return '00000'.substring(0, 6 - c.length) + c
}
function generateColorFromString(str) {
return intToRGB(hashCode(str))
}
</script>
<template>
<USelectMenu
v-model="labels"
by="id"
name="labels"
:options="options"
option-attribute="name"
clearable
multiple
searchable
creatable
>
<template #label>
<template v-if="labels.length">
<span class="flex items-center -space-x-1">
<span v-for="label of labels" :key="label.id" class="flex-shrink-0 w-2 h-2 mt-px rounded-full" :style="{ background: `#${label.color}` }" />
</span>
<span>{{ labels.length }} label{{ labels.length > 1 ? 's' : '' }}</span>
</template>
<template v-else>
<span class="text-gray-500 dark:text-gray-400 truncate">Select labels</span>
</template>
</template>
<template #option="{ option }">
<span
class="flex-shrink-0 w-2 h-2 mt-px rounded-full"
:style="{ background: `#${option.color}` }"
/>
<span class="truncate">{{ option.name }}</span>
</template>
<template #option-create="{ option }">
<span class="flex-shrink-0">New label:</span>
<span
class="flex-shrink-0 w-2 h-2 mt-px rounded-full -mx-1"
:style="{ background: `#${generateColorFromString(option.name)}` }"
/>
<span class="block truncate">{{ option.name }}</span>
</template>
</USelectMenu>
</template>

View File

@@ -156,7 +156,18 @@ Use the `searchableLazy` prop to control the immediacy of data requests.
---
component: 'select-menu-example-search-async'
componentProps:
class: 'w-full lg:w-48'
class: 'w-full lg:w-48'
---
::
## Clearable
Use the `clearable` prop to enable the remove selected option.
::component-example
---
component: 'select-menu-example-clearable'
componentProps:
class: 'w-full lg:w-52'
---
::

View File

@@ -1,21 +1,23 @@
<script setup lang="ts">
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref()
const handleClose = () => {
}
</script>
<template>
<UContainer>
<USelectMenu
v-model="selected"
multiple
clearable
:options="people"
placeholder="Select people"
@clear="handleClose"
/>
<UContainer class="min-h-screen flex items-center">
<UCard class="flex-1" :ui="{ background: 'bg-gray-50 dark:bg-gray-800/50', ring: 'ring-1 ring-gray-300 dark:ring-gray-700', divide: 'divide-y divide-gray-300 dark:divide-gray-700', header: { base: 'font-bold' } }">
<template #header>
Welcome to the playground!
</template>
<p class="text-gray-500 dark:text-gray-400">
Try your components here!
</p>
</UCard>
</UContainer>
</template>
<script setup>
</script>
<style>
body {
@apply antialiased font-sans text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-900;
}
</style>

View File

@@ -39,13 +39,22 @@
<span v-if="label" :class="uiMenu.label">{{ label }}</span>
<span v-else :class="uiMenu.label">{{ placeholder || '&nbsp;' }}</span>
</slot>
<span v-if="canClearValue" :class="clearableWrapperClass">
<slot name="clearable" :selected="selected" :disabled="disabled" :loading="loading" @clear="onClear">
<UButton
:icon="clearableIcon"
size="xs"
class="p-0"
:class="clearableButtonClass"
variant="ghost"
@click.capture.stop="onClear"
/>
</slot>
</span>
<span v-if="(isTrailing && trailingIconName) || $slots.trailing" :class="trailingWrapperIconClass">
<slot
name="trailing"
v-bind="trailingSlotProps()"
>
<UIcon :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" @click="onClear" />
<slot name="trailing" :selected="selected" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
</slot>
</span>
</button>
@@ -152,6 +161,7 @@ import { useFormGroup } from '../../composables/useFormGroup'
import { get, mergeConfig } from '../../utils'
import { useInjectButtonGroup } from '../../composables/useButtonGroup'
import type { SelectSize, SelectColor, SelectVariant, PopperOptions, Strategy, DeepPartial } from '../../types/index'
import type { Button } from '../../types/button'
// @ts-expect-error
import appConfig from '#build/app.config'
import { select, selectMenu } from '#ui/ui.config'
@@ -344,11 +354,8 @@ export default defineComponent({
clearableIcon: {
type: String,
default: () => config.default.clerableIcon
},
closeOnClear: {
type: Boolean,
default: () => configMenu.default.closeOnClear
}
},
emits: ['update:modelValue', 'update:query', 'open', 'close', 'change', 'clear'],
setup(props, { emit, slots }) {
@@ -463,10 +470,22 @@ export default defineComponent({
const canClearValue = computed(() => props.clearable && (Array.isArray(selected.value) ? selected.value.length > 0 : !!selected.value))
const clearableWrapperClass = computed(() => {
return twJoin(
ui.value.icon.clearable.wrapper,
ui.value.icon.clearable.padding[size.value]
)
})
const clearableButtonClass = computed(() => {
return twJoin(
ui.value.icon.base,
color.value && appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
props.loading && ui.value.icon.loading
)
})
const trailingIconName = computed(() => {
if (canClearValue.value) {
return props.clearableIcon
}
if (props.loading && !isLeading.value) {
return props.loadingIcon
}
@@ -598,11 +617,8 @@ export default defineComponent({
query.value = event.target.value
}
function onClear(e: Event) {
function onClear() {
if (canClearValue.value) {
if (container.value && !props.closeOnClear) {
e.stopPropagation()
}
emit('update:modelValue', props.multiple ? [] : null)
emit('clear')
emitFormChange()
@@ -658,7 +674,10 @@ export default defineComponent({
query,
onUpdate,
onQueryChange,
trailingSlotProps
trailingSlotProps,
canClearValue,
clearableWrapperClass,
clearableButtonClass
}
}
})

View File

@@ -98,6 +98,18 @@ export default {
'lg': 'px-3.5',
'xl': 'px-3.5'
}
},
clearable: {
wrapper: 'absolute inset-y-0 end-6 flex items-center',
pointer: 'pointer-events-auto',
padding: {
'2xs': 'px-2',
'xs': 'px-2.5',
'sm': 'px-2.5',
'md': 'px-3',
'lg': 'px-3.5',
'xl': 'px-3.5'
}
}
},
default: {

View File

@@ -23,7 +23,6 @@ export default {
default: {
selectedIcon: 'i-heroicons-check-20-solid',
clearSearchOnClose: false,
closeOnClear: true,
showCreateOptionWhen: 'empty',
searchablePlaceholder: {
label: 'Search...'