Skip to content

Commit 7304a37

Browse files
authored
fix(VDateInput): add update-on prop (#21249)
fixes #21245 fixes #20964
1 parent 5d58b69 commit 7304a37

File tree

3 files changed

+139
-8
lines changed

3 files changed

+139
-8
lines changed

packages/api-generator/src/locale/en/VDateInput.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"props": {
33
"hideActions": "Hide the Cancel and OK buttons, and automatically update the value when a date is selected.",
44
"displayFormat": "The format of the date that is displayed in the input. Can use any format [here](/features/dates/#format-options) or a custom function.",
5-
"location": "Specifies the date picker's location. Can combine by using a space separated string."
5+
"location": "Specifies the date picker's location. Can combine by using a space separated string.",
6+
"updateOn": "Specifies when the text input should update the model value. If empty, the text field will go into read-only state."
67
}
78
}

packages/vuetify/src/labs/VDateInput/VDateInput.tsx

+27-7
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,14 @@ export const makeVDateInputProps = propsFactory({
3939
type: String as PropType<StrategyProps['location']>,
4040
default: 'bottom start',
4141
},
42+
updateOn: {
43+
type: Array as PropType<('blur' | 'enter')[]>,
44+
default: () => ['blur', 'enter'],
45+
},
4246

43-
...makeDisplayProps(),
47+
...makeDisplayProps({
48+
mobile: null,
49+
}),
4450
...makeFocusProps(),
4551
...makeVConfirmEditProps({
4652
hideActions: true,
@@ -121,7 +127,12 @@ export const VDateInput = genericComponent<VDateInputSlots>()({
121127
})
122128

123129
const isInteractive = computed(() => !props.disabled && !props.readonly)
124-
const isReadonly = computed(() => !(mobile.value && isEditingInput.value) && props.readonly)
130+
131+
const isReadonly = computed(() => {
132+
if (!props.updateOn.length) return true
133+
134+
return !(mobile.value && isEditingInput.value) && props.readonly
135+
})
125136

126137
watch(menu, val => {
127138
if (val) return
@@ -135,13 +146,12 @@ export const VDateInput = genericComponent<VDateInputSlots>()({
135146

136147
if (!menu.value || !isFocused.value) {
137148
menu.value = true
138-
139149
return
140150
}
141151

142-
const target = e.target as HTMLInputElement
143-
144-
model.value = adapter.isValid(target.value) ? target.value : null
152+
if (props.updateOn.includes('enter')) {
153+
onUserInput(e.target as HTMLInputElement)
154+
}
145155
}
146156

147157
function onClick (e: MouseEvent) {
@@ -172,7 +182,11 @@ export const VDateInput = genericComponent<VDateInputSlots>()({
172182
model.value = null
173183
}
174184

175-
function onBlur () {
185+
function onBlur (e: FocusEvent) {
186+
if (props.updateOn.includes('blur')) {
187+
onUserInput(e.target as HTMLInputElement)
188+
}
189+
176190
blur()
177191

178192
// When in mobile mode and editing is done (due to keyboard dismissal), close the menu
@@ -182,6 +196,12 @@ export const VDateInput = genericComponent<VDateInputSlots>()({
182196
}
183197
}
184198

199+
function onUserInput ({ value }: HTMLInputElement) {
200+
if (value && !adapter.isValid(value)) return
201+
202+
model.value = !value ? null : value
203+
}
204+
185205
useRender(() => {
186206
const confirmEditProps = VConfirmEdit.filterProps(props)
187207
const datePickerProps = VDatePicker.filterProps(omit(props, ['active', 'location', 'rounded']))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { VDateInput } from '../VDateInput'
2+
3+
// Utilities
4+
import { mount } from '@vue/test-utils'
5+
import { createVuetify } from '@/framework'
6+
7+
// global.ResizeObserver = require('resize-observer-polyfill')
8+
9+
describe('VDateInput', () => {
10+
const vuetify = createVuetify()
11+
12+
afterEach(() => {
13+
vi.clearAllMocks()
14+
})
15+
16+
function mountFunction (component: any, options = {}) {
17+
return mount(component, {
18+
global: {
19+
plugins: [vuetify],
20+
},
21+
...options,
22+
})
23+
}
24+
25+
describe('update-on prop', () => {
26+
const TEST_DATE = '2025-01-01'
27+
28+
it('should update modelValue only on enter key press', async () => {
29+
const wrapper = mountFunction(
30+
<VDateInput
31+
updateOn={['enter']}
32+
modelValue={ null }
33+
/>
34+
)
35+
const input = wrapper.find('input')
36+
37+
await input.trigger('click')
38+
await input.trigger('focus')
39+
await input.setValue(TEST_DATE)
40+
41+
await input.trigger('keydown', { key: 'Enter' })
42+
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
43+
44+
await input.trigger('blur')
45+
expect(wrapper.emitted('update:modelValue')).toHaveLength(1)
46+
})
47+
48+
it('should update modelValue only on blur event', async () => {
49+
const wrapper = mountFunction(
50+
<VDateInput
51+
updateOn={['blur']}
52+
modelValue={ null }
53+
/>
54+
)
55+
const input = wrapper.find('input')
56+
57+
await input.trigger('click')
58+
await input.trigger('focus')
59+
await input.setValue(TEST_DATE)
60+
61+
await input.trigger('keydown', { key: 'Enter' })
62+
expect(wrapper.emitted('update:modelValue')).toBeFalsy()
63+
64+
await input.trigger('blur')
65+
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
66+
})
67+
68+
it('should update modelValue on both enter key press and blur event', async () => {
69+
const wrapper = mountFunction(
70+
<VDateInput
71+
updateOn={['enter', 'blur']}
72+
modelValue={ null }
73+
/>
74+
)
75+
const input = wrapper.find('input')
76+
77+
await input.trigger('click')
78+
await input.trigger('focus')
79+
await input.setValue(TEST_DATE)
80+
81+
await input.trigger('keydown', { key: 'Enter' })
82+
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
83+
84+
await input.trigger('blur')
85+
expect(wrapper.emitted('update:modelValue')).toHaveLength(2)
86+
})
87+
88+
it('should make the input readonly and prevent value updates', async () => {
89+
const wrapper = mountFunction(
90+
<VDateInput
91+
updateOn={[]}
92+
modelValue={ null }
93+
/>
94+
)
95+
const input = wrapper.find('input')
96+
97+
expect(input.attributes('readonly')).toBeDefined()
98+
expect(input.element.readOnly).toBe(true)
99+
100+
await input.trigger('click')
101+
await input.trigger('focus')
102+
103+
await input.setValue(TEST_DATE)
104+
await input.trigger('keydown', { key: 'Enter' })
105+
await input.trigger('blur')
106+
107+
expect(wrapper.emitted('update:modelValue')).toBeFalsy()
108+
})
109+
})
110+
})

0 commit comments

Comments
 (0)