Skip to content
Draft
22 changes: 22 additions & 0 deletions docs/content/docs/2.components/input-menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,28 @@ props:
---
::

### Free input

Use the `allowFreeInput` prop to let users type any value, even if it is not present in the items list.

::component-code
---
prettier: true
ignore:
- items
external:
- items
props:
placeholder: 'Type or select a status'
allowFreeInput: true
items:
- Backlog
- Todo
- In Progress
- Done
---
::

## Examples

### With items type
Expand Down
33 changes: 25 additions & 8 deletions src/runtime/components/InputMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ export interface InputMenuProps<T extends ArrayOrNested<InputMenuItem> = ArrayOr
multiple?: M & boolean
/** Highlight the ring color like a focus state. */
highlight?: boolean
/**
* Allow free input that is not present in the items list.
* When true, the value typed by the user is directly reflected in `v-model`.
*/
allowFreeInput?: boolean
/**
* Determines if custom user input that does not exist in options can be added.
* @defaultValue false
Expand Down Expand Up @@ -238,10 +243,14 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.inputMenu ||
const items = computed(() => groups.value.flatMap(group => group) as T[])

function displayValue(value: GetItemValue<T, VK>): string {
return getDisplayValue<T[], GetItemValue<T, VK>>(items.value, value, {
labelKey: props.labelKey,
valueKey: props.valueKey
}) ?? ''
if (props.allowFreeInput) {
return String(value ?? '')
} else {
return getDisplayValue<T[], GetItemValue<T, VK>>(items.value, value, {
labelKey: props.labelKey,
valueKey: props.valueKey
}) ?? ''
}
}

const groups = computed<InputMenuItem[][]>(() =>
Expand Down Expand Up @@ -325,11 +334,19 @@ function onUpdate(value: any) {
emitFormChange()
emitFormInput()

if (props.resetSearchTermOnSelect) {
if (props.resetSearchTermOnSelect && !props.allowFreeInput) {
searchTerm.value = ''
}
}

function onSearchTermUpdate(value: string) {
searchTerm.value = value
if (props.allowFreeInput) {
emits('update:modelValue', value as GetModelValue<T, VK, M>)
onUpdate(value)
}
}

function onBlur(event: FocusEvent) {
emits('blur', event)
emitFormBlur()
Expand All @@ -351,7 +368,7 @@ function onUpdateOpen(value: boolean) {

// Since we use `displayValue` prop inside ComboboxInput we should reset searchTerm manually
// https://reka-ui.com/docs/components/combobox#api-reference
if (props.resetSearchTermOnBlur) {
if (props.resetSearchTermOnBlur && !props.allowFreeInput) {
const STATE_ANIMATION_DELAY_MS = 100

timeoutId = setTimeout(() => {
Expand Down Expand Up @@ -478,7 +495,7 @@ defineExpose({
:required="required"
@blur="onBlur"
@focus="onFocus"
@update:model-value="searchTerm = $event"
@update:model-value="onSearchTermUpdate"
/>

<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
Expand All @@ -499,7 +516,7 @@ defineExpose({
<ComboboxContent :class="ui.content({ class: props.ui?.content })" v-bind="contentProps" @focus-outside.prevent>
<slot name="content-top" />

<ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })">
<ComboboxEmpty v-if="!props.allowFreeInput" :class="ui.empty({ class: props.ui?.empty })">
<slot name="empty" :search-term="searchTerm">
{{ searchTerm ? t('inputMenu.noMatch', { searchTerm }) : t('inputMenu.noData') }}
</slot>
Expand Down
Loading