Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion src/runtime/components/InputMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/input-menu'
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
import type { AvatarProps, ChipProps, IconProps, InputProps } from '../types'
import type { ModelModifiers } from '../types/input'
import type { InputHTMLAttributes } from '../types/html'
import type { AcceptableValue, ArrayOrNested, GetItemKeys, GetItemValue, GetModelValue, GetModelValueEmits, NestedItem, EmitsToProps } from '../types/utils'
import type { ComponentConfig } from '../types/tv'
Expand Down Expand Up @@ -129,6 +130,7 @@ export interface InputMenuProps<T extends ArrayOrNested<InputMenuItem> = ArrayOr
defaultValue?: GetModelValue<T, VK, M>
/** The controlled value of the InputMenu. Can be binded-with with `v-model`. */
modelValue?: GetModelValue<T, VK, M>
modelModifiers?: Omit<ModelModifiers<GetModelValue<T, VK, M>>, 'lazy'>
/** Whether multiple options can be selected or not. */
multiple?: M & boolean
/** Highlight the ring color like a focus state. */
Expand Down Expand Up @@ -201,7 +203,7 @@ import { useComponentIcons } from '../composables/useComponentIcons'
import { useFormField } from '../composables/useFormField'
import { useLocale } from '../composables/useLocale'
import { usePortal } from '../composables/usePortal'
import { compare, get, getDisplayValue, isArrayOfArray } from '../utils'
import { compare, get, getDisplayValue, isArrayOfArray, looseToNumber } from '../utils'
import { getEstimateSize } from '../utils/virtualizer'
import { tv } from '../utils/tv'
import UIcon from './Icon.vue'
Expand Down Expand Up @@ -359,6 +361,23 @@ function onUpdate(value: any) {
if (toRaw(props.modelValue) === value) {
return
}

if (props.modelModifiers?.trim) {
value = value?.trim() ?? null
}

if (props.modelModifiers?.number) {
value = looseToNumber(value)
}

if (props.modelModifiers?.nullable) {
value ??= null
}

if (props.modelModifiers?.optional) {
value ??= undefined
}

// @ts-expect-error - 'target' does not exist in type 'EventInit'
const event = new Event('change', { target: { value } })
emits('change', event)
Expand Down
20 changes: 19 additions & 1 deletion src/runtime/components/Select.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/select'
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
import type { AvatarProps, ChipProps, IconProps, InputProps } from '../types'
import type { ModelModifiers } from '../types/input'
import type { ButtonHTMLAttributes } from '../types/html'
import type { AcceptableValue, ArrayOrNested, GetItemKeys, GetItemValue, GetModelValue, GetModelValueEmits, NestedItem, EmitsToProps } from '../types/utils'
import type { ComponentConfig } from '../types/tv'
Expand Down Expand Up @@ -97,6 +98,7 @@ export interface SelectProps<T extends ArrayOrNested<SelectItem> = ArrayOrNested
defaultValue?: GetModelValue<T, VK, M>
/** The controlled value of the Select. Can be bind as `v-model`. */
modelValue?: GetModelValue<T, VK, M>
modelModifiers?: Omit<ModelModifiers<GetModelValue<T, VK, M>>, 'lazy'>
/** Whether multiple options can be selected or not. */
multiple?: M & boolean
/** Highlight the ring color like a focus state. */
Expand Down Expand Up @@ -144,7 +146,7 @@ import { useFieldGroup } from '../composables/useFieldGroup'
import { useComponentIcons } from '../composables/useComponentIcons'
import { useFormField } from '../composables/useFormField'
import { usePortal } from '../composables/usePortal'
import { get, getDisplayValue, isArrayOfArray } from '../utils'
import { get, getDisplayValue, isArrayOfArray, looseToNumber } from '../utils'
import { tv } from '../utils/tv'
import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
Expand Down Expand Up @@ -231,6 +233,22 @@ onMounted(() => {
})

function onUpdate(value: any) {
if (props.modelModifiers?.trim) {
value = value?.trim() ?? null
}

if (props.modelModifiers?.number) {
value = looseToNumber(value)
}

if (props.modelModifiers?.nullable) {
value ??= null
}

if (props.modelModifiers?.optional) {
value ??= undefined
}

// @ts-expect-error - 'target' does not exist in type 'EventInit'
const event = new Event('change', { target: { value } })
emits('change', event)
Expand Down
21 changes: 20 additions & 1 deletion src/runtime/components/SelectMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/select-menu'
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
import type { AvatarProps, ChipProps, IconProps, InputProps } from '../types'
import type { ModelModifiers } from '../types/input'
import type { ButtonHTMLAttributes } from '../types/html'
import type { AcceptableValue, ArrayOrNested, GetItemKeys, GetItemValue, GetModelValue, GetModelValueEmits, NestedItem, EmitsToProps } from '../types/utils'
import type { ComponentConfig } from '../types/tv'
Expand Down Expand Up @@ -122,6 +123,7 @@ export interface SelectMenuProps<T extends ArrayOrNested<SelectMenuItem> = Array
defaultValue?: GetModelValue<T, VK, M>
/** The controlled value of the SelectMenu. Can be binded-with with `v-model`. */
modelValue?: GetModelValue<T, VK, M>
modelModifiers?: Omit<ModelModifiers<GetModelValue<T, VK, M>>, 'lazy'>
/** Whether multiple options can be selected or not. */
multiple?: M & boolean
/** Highlight the ring color like a focus state. */
Expand Down Expand Up @@ -193,7 +195,7 @@ import { useComponentIcons } from '../composables/useComponentIcons'
import { useFormField } from '../composables/useFormField'
import { useLocale } from '../composables/useLocale'
import { usePortal } from '../composables/usePortal'
import { compare, get, getDisplayValue, isArrayOfArray } from '../utils'
import { compare, get, getDisplayValue, isArrayOfArray, looseToNumber } from '../utils'
import { getEstimateSize } from '../utils/virtualizer'
import { tv } from '../utils/tv'
import UIcon from './Icon.vue'
Expand Down Expand Up @@ -360,6 +362,23 @@ function onUpdate(value: any) {
if (toRaw(props.modelValue) === value) {
return
}

if (props.modelModifiers?.trim) {
value = value?.trim() ?? null
}

if (props.modelModifiers?.number) {
value = looseToNumber(value)
}

if (props.modelModifiers?.nullable) {
value ??= null
}

if (props.modelModifiers?.optional) {
value ??= undefined
}

// @ts-expect-error - 'target' does not exist in type 'EventInit'
const event = new Event('change', { target: { value } })
emits('change', event)
Expand Down
16 changes: 16 additions & 0 deletions test/components/InputMenu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,22 @@ describe('InputMenu', () => {
expect(html).toMatchSnapshot()
})

it.each([
['with .trim modifier', { props: { modelModifiers: { trim: true } } }, { input: 'input ', expected: 'input' }],
['with .number modifier', { props: { modelModifiers: { number: true } } }, { input: '42', expected: 42 }],
['with .nullable modifier', { props: { modelModifiers: { nullable: true } } }, { input: null, expected: null }],
['with .optional modifier', { props: { modelModifiers: { optional: true } } }, { input: undefined, expected: undefined }]
])('%s works', async (_nameOrHtml: string, options: { props?: any, slots?: any }, spec: { input: any, expected: any }) => {
const wrapper = mount(InputMenu, {
...options
})

const input = wrapper.findComponent({ name: 'ComboboxRoot' })
await input.setValue(spec.input)

expect(wrapper.emitted()).toMatchObject({ 'update:modelValue': [[spec.expected]] })
})

it('passes accessibility tests', async () => {
const wrapper = await mountSuspended(InputMenu, {
props: {
Expand Down
16 changes: 16 additions & 0 deletions test/components/Select.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,22 @@ describe('Select', () => {
expect(html).toMatchSnapshot()
})

it.each([
['with .trim modifier', { props: { modelModifiers: { trim: true } } }, { input: 'input ', expected: 'input' }],
['with .number modifier', { props: { modelModifiers: { number: true } } }, { input: '42', expected: 42 }],
['with .nullable modifier', { props: { modelModifiers: { nullable: true } } }, { input: null, expected: null }],
['with .optional modifier', { props: { modelModifiers: { optional: true } } }, { input: undefined, expected: undefined }]
])('%s works', async (_nameOrHtml: string, options: { props?: any, slots?: any }, spec: { input: any, expected: any }) => {
const wrapper = mount(Select, {
...options
})

const select = wrapper.findComponent({ name: 'SelectRoot' })
await select.setValue(spec.input)

expect(wrapper.emitted()).toMatchObject({ 'update:modelValue': [[spec.expected]] })
})

it('passes accessibility tests', async () => {
const wrapper = await mountSuspended(Select, {
props: {
Expand Down
16 changes: 16 additions & 0 deletions test/components/SelectMenu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,22 @@ describe('SelectMenu', () => {
expect(html).toMatchSnapshot()
})

it.each([
['with .trim modifier', { props: { modelModifiers: { trim: true } } }, { input: 'input ', expected: 'input' }],
['with .number modifier', { props: { modelModifiers: { number: true } } }, { input: '42', expected: 42 }],
['with .nullable modifier', { props: { modelModifiers: { nullable: true } } }, { input: null, expected: null }],
['with .optional modifier', { props: { modelModifiers: { optional: true } } }, { input: undefined, expected: undefined }]
])('%s works', async (_nameOrHtml: string, options: { props?: any, slots?: any }, spec: { input: any, expected: any }) => {
const wrapper = mount(SelectMenu, {
...options
})

const selectMenu = wrapper.findComponent({ name: 'ComboboxRoot' })
await selectMenu.setValue(spec.input)

expect(wrapper.emitted()).toMatchObject({ 'update:modelValue': [[spec.expected]] })
})

it('passes accessibility tests', async () => {
const wrapper = await mountSuspended(SelectMenu, {
props: {
Expand Down
Loading