Skip to content

Commit ec6b8ec

Browse files
feat(InputMenu/SelectMenu): add clear prop (#5643)
1 parent c4cb0b1 commit ec6b8ec

File tree

14 files changed

+584
-17
lines changed

14 files changed

+584
-17
lines changed

docs/content/docs/2.components/input-menu.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,67 @@ You can customize this icon globally in your `vite.config.ts` under `ui.icons.ch
488488
:::
489489
::
490490

491+
### Clear :badge{label="Soon" class="align-text-top"}
492+
493+
Use the `clear` prop to display a clear button when a value is selected.
494+
495+
::component-code
496+
---
497+
prettier: true
498+
ignore:
499+
- items
500+
- modelValue
501+
external:
502+
- items
503+
- modelValue
504+
props:
505+
modelValue: 'Backlog'
506+
clear: true
507+
items:
508+
- Backlog
509+
- Todo
510+
- In Progress
511+
- Done
512+
---
513+
::
514+
515+
### Clear Icon :badge{label="Soon" class="align-text-top"}
516+
517+
Use the `clear-icon` prop to customize the clear button [Icon](/docs/components/icon). Defaults to `i-lucide-x`.
518+
519+
::component-code
520+
---
521+
prettier: true
522+
ignore:
523+
- items
524+
- modelValue
525+
external:
526+
- items
527+
- modelValue
528+
props:
529+
modelValue: 'Backlog'
530+
clear: true
531+
clearIcon: 'i-lucide-trash'
532+
items:
533+
- Backlog
534+
- Todo
535+
- In Progress
536+
- Done
537+
---
538+
::
539+
540+
::framework-only
541+
#nuxt
542+
:::tip{to="/docs/getting-started/integrations/icons/nuxt#theme"}
543+
You can customize this icon globally in your `app.config.ts` under `ui.icons.close` key.
544+
:::
545+
546+
#vue
547+
:::tip{to="/docs/getting-started/integrations/icons/vue#theme"}
548+
You can customize this icon globally in your `vite.config.ts` under `ui.icons.close` key.
549+
:::
550+
::
551+
491552
### Avatar
492553

493554
Use the `avatar` prop to show an [Avatar](/docs/components/avatar) inside the InputMenu.

docs/content/docs/2.components/select-menu.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,71 @@ You can customize this icon globally in your `vite.config.ts` under `ui.icons.ch
527527
:::
528528
::
529529

530+
### Clear :badge{label="Soon" class="align-text-top"}
531+
532+
Use the `clear` prop to display a clear button when a value is selected.
533+
534+
::component-code
535+
---
536+
prettier: true
537+
ignore:
538+
- items
539+
- modelValue
540+
- class
541+
external:
542+
- items
543+
- modelValue
544+
props:
545+
modelValue: 'Backlog'
546+
clear: true
547+
items:
548+
- Backlog
549+
- Todo
550+
- In Progress
551+
- Done
552+
class: 'w-48'
553+
---
554+
::
555+
556+
### Clear Icon :badge{label="Soon" class="align-text-top"}
557+
558+
Use the `clear-icon` prop to customize the clear button [Icon](/docs/components/icon). Defaults to `i-lucide-x`.
559+
560+
::component-code
561+
---
562+
prettier: true
563+
ignore:
564+
- items
565+
- modelValue
566+
- class
567+
external:
568+
- items
569+
- modelValue
570+
props:
571+
modelValue: 'Backlog'
572+
clear: true
573+
clearIcon: 'i-lucide-trash'
574+
items:
575+
- Backlog
576+
- Todo
577+
- In Progress
578+
- Done
579+
class: 'w-48'
580+
---
581+
::
582+
583+
::framework-only
584+
#nuxt
585+
:::tip{to="/docs/getting-started/integrations/icons/nuxt#theme"}
586+
You can customize this icon globally in your `app.config.ts` under `ui.icons.close` key.
587+
:::
588+
589+
#vue
590+
:::tip{to="/docs/getting-started/integrations/icons/vue#theme"}
591+
You can customize this icon globally in your `vite.config.ts` under `ui.icons.close` key.
592+
:::
593+
::
594+
530595
### Avatar
531596

532597
Use the `avatar` prop to display an [Avatar](/docs/components/avatar) inside the SelectMenu.

playgrounds/nuxt/app/pages/components/input-menu.vue

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,16 @@ const valueMultiple = ref([fruits[0]!, vegetables[0]!])
7070

7171
<Matrix v-slot="props" :attrs="attrs">
7272
<UInputMenu v-model="value" :items="items" autofocus v-bind="props" />
73-
<UInputMenu :default-value="value" :items="items" v-bind="props" />
73+
<UInputMenu :default-value="value" :items="items" v-bind="props" clear />
7474
<UInputMenu v-model="valueMultiple" multiple placeholder="Multiple" :items="items" v-bind="props" />
75-
<UInputMenu :default-value="valueMultiple" multiple placeholder="Multiple" :items="items" v-bind="props" />
75+
<UInputMenu
76+
:default-value="valueMultiple"
77+
multiple
78+
placeholder="Multiple"
79+
:items="items"
80+
v-bind="props"
81+
clear
82+
/>
7683
<UInputMenu placeholder="Highlight" highlight :items="items" v-bind="props" />
7784
<UInputMenu placeholder="Disabled" disabled :items="items" v-bind="props" />
7885
<UInputMenu placeholder="Required" required :items="items" v-bind="props" />

playgrounds/nuxt/app/pages/components/select-menu.vue

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,16 @@ const valueMultiple = ref([fruits[0]!, vegetables[0]!])
7070

7171
<Matrix v-slot="props" :attrs="attrs">
7272
<USelectMenu v-model="value" :items="items" autofocus v-bind="props" />
73-
<USelectMenu :default-value="value" :items="items" v-bind="props" />
73+
<USelectMenu :default-value="value" :items="items" v-bind="props" clear />
7474
<USelectMenu v-model="valueMultiple" multiple placeholder="Multiple" :items="items" v-bind="props" />
75-
<USelectMenu :default-value="valueMultiple" multiple placeholder="Multiple" :items="items" v-bind="props" />
75+
<USelectMenu
76+
:default-value="valueMultiple"
77+
multiple
78+
placeholder="Multiple"
79+
:items="items"
80+
v-bind="props"
81+
clear
82+
/>
7683
<USelectMenu placeholder="Highlight" highlight :items="items" v-bind="props" />
7784
<USelectMenu placeholder="Disabled" disabled :items="items" v-bind="props" />
7885
<USelectMenu placeholder="Required" required :items="items" v-bind="props" />

src/runtime/components/InputMenu.vue

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps, Combob
33
import type { AppConfig } from '@nuxt/schema'
44
import theme from '#build/ui/input-menu'
55
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
6-
import type { AvatarProps, ChipProps, IconProps, InputProps } from '../types'
6+
import type { AvatarProps, ButtonProps, ChipProps, IconProps, InputProps, LinkPropsKeys } from '../types'
77
import type { ModelModifiers } from '../types/input'
88
import type { InputHTMLAttributes } from '../types/html'
99
import type { AcceptableValue, ArrayOrNested, GetItemKeys, GetItemValue, GetModelValue, GetModelValueEmits, NestedItem, EmitsToProps } from '../types/utils'
@@ -34,7 +34,7 @@ export type InputMenuItem = InputMenuValue | {
3434
[key: string]: any
3535
}
3636
37-
export interface InputMenuProps<T extends ArrayOrNested<InputMenuItem> = ArrayOrNested<InputMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false> extends Pick<ComboboxRootProps<T>, 'open' | 'defaultOpen' | 'disabled' | 'name' | 'resetSearchTermOnBlur' | 'resetSearchTermOnSelect' | 'highlightOnHover' | 'openOnClick' | 'openOnFocus'>, UseComponentIconsProps, /** @vue-ignore */ Omit<InputHTMLAttributes, 'disabled' | 'name' | 'type' | 'placeholder' | 'autofocus' | 'maxlength' | 'minlength' | 'pattern' | 'size' | 'min' | 'max' | 'step'> {
37+
export interface InputMenuProps<T extends ArrayOrNested<InputMenuItem> = ArrayOrNested<InputMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false> extends Pick<ComboboxRootProps<T>, 'open' | 'defaultOpen' | 'disabled' | 'name' | 'resetSearchTermOnBlur' | 'resetSearchTermOnSelect' | 'resetModelValueOnClear' | 'highlightOnHover' | 'openOnClick' | 'openOnFocus'>, UseComponentIconsProps, /** @vue-ignore */ Omit<InputHTMLAttributes, 'disabled' | 'name' | 'type' | 'placeholder' | 'autofocus' | 'maxlength' | 'minlength' | 'pattern' | 'size' | 'min' | 'max' | 'step'> {
3838
/**
3939
* The element or component this component should render as.
4040
* @defaultValue 'div'
@@ -78,6 +78,18 @@ export interface InputMenuProps<T extends ArrayOrNested<InputMenuItem> = ArrayOr
7878
* @IconifyIcon
7979
*/
8080
deleteIcon?: IconProps['name']
81+
/**
82+
* Display a clear button to reset the model value.
83+
* Can be an object to pass additional props to the Button.
84+
* @defaultValue false
85+
*/
86+
clear?: boolean | Partial<Omit<ButtonProps, LinkPropsKeys>>
87+
/**
88+
* The icon displayed in the clear button.
89+
* @defaultValue appConfig.ui.icons.close
90+
* @IconifyIcon
91+
*/
92+
clearIcon?: IconProps['name']
8193
/**
8294
* The content of the menu.
8395
* @defaultValue { side: 'bottom', sideOffset: 8, collisionPadding: 8, position: 'popper' }
@@ -159,6 +171,7 @@ export type InputMenuEmits<A extends ArrayOrNested<InputMenuItem>, VK extends Ge
159171
'blur': [event: FocusEvent]
160172
'focus': [event: FocusEvent]
161173
'create': [item: string]
174+
'clear': []
162175
/** Event handler when highlighted element changes. */
163176
'highlight': [payload: {
164177
ref: HTMLElement
@@ -193,7 +206,7 @@ export interface InputMenuSlots<
193206

194207
<script setup lang="ts" generic="T extends ArrayOrNested<InputMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false">
195208
import { computed, useTemplateRef, toRef, onMounted, toRaw, nextTick } from 'vue'
196-
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxVirtualizer, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, TagsInputRoot, TagsInputItem, TagsInputItemText, TagsInputItemDelete, TagsInputInput, useForwardPropsEmits, useFilter } from 'reka-ui'
209+
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxCancel, ComboboxPortal, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxVirtualizer, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, TagsInputRoot, TagsInputItem, TagsInputItemText, TagsInputItemDelete, TagsInputInput, useForwardPropsEmits, useFilter } from 'reka-ui'
197210
import { defu } from 'defu'
198211
import { isEqual } from 'ohash/utils'
199212
import { reactivePick, createReusableTemplate } from '@vueuse/core'
@@ -208,6 +221,7 @@ import { getEstimateSize } from '../utils/virtualizer'
208221
import { tv } from '../utils/tv'
209222
import UIcon from './Icon.vue'
210223
import UAvatar from './Avatar.vue'
224+
import UButton from './Button.vue'
211225
import UChip from './Chip.vue'
212226
213227
defineOptions({ inheritAttrs: false })
@@ -220,6 +234,7 @@ const props = withDefaults(defineProps<InputMenuProps<T, VK, M>>(), {
220234
descriptionKey: 'description',
221235
resetSearchTermOnBlur: true,
222236
resetSearchTermOnSelect: true,
237+
resetModelValueOnClear: true,
223238
virtualize: false
224239
})
225240
const emits = defineEmits<InputMenuEmits<T, VK, M>>()
@@ -231,10 +246,11 @@ const { t } = useLocale()
231246
const appConfig = useAppConfig() as InputMenu['AppConfig']
232247
const { contains } = useFilter({ sensitivity: 'base' })
233248
234-
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'open', 'defaultOpen', 'required', 'multiple', 'resetSearchTermOnBlur', 'resetSearchTermOnSelect', 'highlightOnHover', 'openOnClick', 'openOnFocus'), emits)
249+
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'open', 'defaultOpen', 'required', 'multiple', 'resetSearchTermOnBlur', 'resetSearchTermOnSelect', 'resetModelValueOnClear', 'highlightOnHover', 'openOnClick', 'openOnFocus'), emits)
235250
const portalProps = usePortal(toRef(() => props.portal))
236251
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8, position: 'popper' }) as ComboboxContentProps)
237252
const arrowProps = toRef(() => props.arrow as ComboboxArrowProps)
253+
const clearProps = computed(() => typeof props.clear === 'object' ? props.clear : {} as Partial<Omit<ButtonProps, LinkPropsKeys>>)
238254
const virtualizerProps = toRef(() => {
239255
if (!props.virtualize) return false
240256
@@ -458,6 +474,17 @@ function isInputItem(item: InputMenuItem): item is Exclude<InputMenuItem, InputM
458474
return typeof item === 'object' && item !== null
459475
}
460476
477+
function isModelValueEmpty(modelValue: GetModelValue<T, VK, M>): boolean {
478+
if (props.multiple && Array.isArray(modelValue)) {
479+
return modelValue.length === 0
480+
}
481+
return modelValue === undefined || modelValue === null || modelValue === ''
482+
}
483+
484+
function onClear() {
485+
emits('clear')
486+
}
487+
461488
defineExpose({
462489
inputRef: toRef(() => inputRef.value?.$el as HTMLInputElement)
463490
})
@@ -609,9 +636,23 @@ defineExpose({
609636
</slot>
610637
</span>
611638

612-
<ComboboxTrigger v-if="isTrailing || !!slots.trailing" data-slot="trailing" :class="ui.trailing({ class: props.ui?.trailing })">
639+
<ComboboxTrigger v-if="isTrailing || !!slots.trailing || !!clear" data-slot="trailing" :class="ui.trailing({ class: props.ui?.trailing })">
613640
<slot name="trailing" :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open" :ui="ui">
614-
<UIcon v-if="trailingIconName" :name="trailingIconName" data-slot="trailingIcon" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
641+
<ComboboxCancel v-if="!!clear && !isModelValueEmpty(modelValue as GetModelValue<T, VK, M>)" as-child>
642+
<UButton
643+
as="span"
644+
:icon="clearIcon || appConfig.ui.icons.close"
645+
variant="link"
646+
color="neutral"
647+
tabindex="-1"
648+
v-bind="clearProps"
649+
data-slot="trailingClear"
650+
:class="ui.trailingClear({ class: props.ui?.trailingClear })"
651+
@click.stop="onClear"
652+
/>
653+
</ComboboxCancel>
654+
655+
<UIcon v-else-if="trailingIconName" :name="trailingIconName" data-slot="trailingIcon" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
615656
</slot>
616657
</ComboboxTrigger>
617658
</ComboboxAnchor>

0 commit comments

Comments
 (0)