Skip to content

Commit d94c003

Browse files
authored
feat(VTextField, VTextarea): add autocomplete prop (#21359)
resolves #21353
1 parent 15a5c96 commit d94c003

File tree

8 files changed

+139
-5
lines changed

8 files changed

+139
-5
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
22
"props": {
3-
"autocomplete": "Filter the items in the list based on user input.",
43
"cacheItems": "Keeps a local _unique_ copy of all items that have been passed through the **items** prop.",
54
"chips": "Changes display of selections to chips.",
65
"closableChips": "Enables the [closable](/api/v-chip/#props-closable) prop on all [v-chip](/components/chips/) components.",
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"props": {
3+
"autocomplete": "Helps influence browser's suggestions. Special value **suppress** manipulates fields `name` attribute while **off** relies on browser's good will to stop suggesting values. Any other value is passed to the native `autocomplete` on the underlying element."
4+
}
5+
}

packages/docs/src/data/new-in.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
},
1515
"VAutocomplete": {
1616
"props": {
17+
"autocomplete": "3.10.0",
1718
"autoSelectFirst": "3.3.0",
1819
"clearOnSelect": "3.5.0",
1920
"listProps": "3.5.0"
@@ -57,6 +58,7 @@
5758
},
5859
"VCombobox": {
5960
"props": {
61+
"autocomplete": "3.10.0",
6062
"clearOnSelect": "3.5.0",
6163
"listProps": "3.5.0"
6264
}
@@ -141,6 +143,11 @@
141143
"persistent": "3.6.0"
142144
}
143145
},
146+
"VNumberInput": {
147+
"props": {
148+
"autocomplete": "3.10.0"
149+
}
150+
},
144151
"VOverlay": {
145152
"props": {
146153
"stickToTarget": "3.10.0"
@@ -160,6 +167,7 @@
160167
},
161168
"VSelect": {
162169
"props": {
170+
"autocomplete": "3.10.0",
163171
"listProps": "3.5.0",
164172
"noAutoScroll": "3.9.0"
165173
}
@@ -193,6 +201,11 @@
193201
"text": "3.2.0"
194202
}
195203
},
204+
"VTextField": {
205+
"props": {
206+
"autocomplete": "3.10.0"
207+
}
208+
},
196209
"VTimePicker": {
197210
"props": {
198211
"hideTitle": "3.10.0",

packages/vuetify/src/components/VTextField/VTextField.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { makeVFieldProps, VField } from '@/components/VField/VField'
77
import { makeVInputProps, VInput } from '@/components/VInput/VInput'
88

99
// Composables
10+
import { makeAutocompleteProps, useAutocomplete } from '@/composables/autocomplete'
1011
import { useAutofocus } from '@/composables/autofocus'
1112
import { useFocus } from '@/composables/focus'
1213
import { forwardRefs } from '@/composables/forwardRefs'
@@ -43,6 +44,7 @@ export const makeVTextFieldProps = propsFactory({
4344
},
4445
modelModifiers: Object as PropType<Record<string, boolean>>,
4546

47+
...makeAutocompleteProps(),
4648
...makeVInputProps(),
4749
...makeVFieldProps(),
4850
}, 'VTextField')
@@ -94,13 +96,18 @@ export const VTextField = genericComponent<VTextFieldSlots>()({
9496
const vInputRef = ref<VInput>()
9597
const vFieldRef = ref<VField>()
9698
const inputRef = ref<HTMLInputElement>()
99+
const autocomplete = useAutocomplete(props)
97100
const isActive = computed(() => (
98101
activeTypes.includes(props.type) ||
99102
props.persistentPlaceholder ||
100103
isFocused.value ||
101104
props.active
102105
))
103106
function onFocus () {
107+
if (autocomplete.isSuppressing.value) {
108+
autocomplete.update()
109+
}
110+
104111
if (!isFocused.value) focus()
105112

106113
nextTick(() => {
@@ -217,7 +224,8 @@ export const VTextField = genericComponent<VTextFieldSlots>()({
217224
autofocus={ props.autofocus }
218225
readonly={ isReadonly.value }
219226
disabled={ isDisabled.value }
220-
name={ props.name }
227+
name={ autocomplete.fieldName.value }
228+
autocomplete={ autocomplete.fieldAutocomplete.value }
221229
placeholder={ props.placeholder }
222230
size={ 1 }
223231
type={ props.type }

packages/vuetify/src/components/VTextarea/VTextarea.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { makeVFieldProps } from '@/components/VField/VField'
99
import { makeVInputProps, VInput } from '@/components/VInput/VInput'
1010

1111
// Composables
12+
import { makeAutocompleteProps, useAutocomplete } from '@/composables/autocomplete'
1213
import { useAutofocus } from '@/composables/autofocus'
1314
import { useFocus } from '@/composables/focus'
1415
import { forwardRefs } from '@/composables/forwardRefs'
@@ -49,6 +50,7 @@ export const makeVTextareaProps = propsFactory({
4950
suffix: String,
5051
modelModifiers: Object as PropType<Record<string, boolean>>,
5152

53+
...makeAutocompleteProps(),
5254
...makeVInputProps(),
5355
...makeVFieldProps(),
5456
}, 'VTextarea')
@@ -99,13 +101,18 @@ export const VTextarea = genericComponent<VTextareaSlots>()({
99101
const vFieldRef = ref<VInput>()
100102
const controlHeight = shallowRef('')
101103
const textareaRef = ref<HTMLInputElement>()
104+
const autocomplete = useAutocomplete(props)
102105
const isActive = computed(() => (
103106
props.persistentPlaceholder ||
104107
isFocused.value ||
105108
props.active
106109
))
107110

108111
function onFocus () {
112+
if (autocomplete.isSuppressing.value) {
113+
autocomplete.update()
114+
}
115+
109116
if (textareaRef.value !== document.activeElement) {
110117
textareaRef.value?.focus()
111118
}
@@ -283,7 +290,8 @@ export const VTextarea = genericComponent<VTextareaSlots>()({
283290
disabled={ isDisabled.value }
284291
placeholder={ props.placeholder }
285292
rows={ props.rows }
286-
name={ props.name }
293+
name={ autocomplete.fieldName.value }
294+
autocomplete={ autocomplete.fieldAutocomplete.value }
287295
onFocus={ onFocus }
288296
onBlur={ blur }
289297
{ ...slotProps }

packages/vuetify/src/components/VTextarea/__tests__/VTextarea.spec.browser.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ describe('VTextarea', () => {
1919
</Application>
2020
))
2121

22-
const el = screen.getByCSS('#input-v-0')
22+
const el = screen.getByCSS('textarea[rows]')
2323

2424
expect(el.offsetHeight).toBe(56)
2525

@@ -43,7 +43,7 @@ describe('VTextarea', () => {
4343
</Application>
4444
))
4545

46-
const el = screen.getByCSS('#input-v-0')
46+
const el = screen.getByCSS('textarea[rows]')
4747

4848
expect(el.offsetHeight).toBe(56)
4949

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Composables
2+
import { makeAutocompleteProps, useAutocomplete } from '../autocomplete'
3+
4+
// Utilities
5+
import { mount } from '@vue/test-utils'
6+
import { defineComponent, h, nextTick } from 'vue'
7+
8+
// Types
9+
import type { InputAutocompleteProps } from '../autocomplete'
10+
11+
describe('input-autocomplete', () => {
12+
function mountFunction (props: Partial<InputAutocompleteProps> = {}) {
13+
return mount(defineComponent({
14+
props: {
15+
name: String,
16+
...makeAutocompleteProps(),
17+
},
18+
setup (props) {
19+
const { fieldAutocomplete, fieldName, isSuppressing, update } = useAutocomplete(props)
20+
21+
return () => h('input', {
22+
autocomplete: fieldAutocomplete.value,
23+
name: fieldName.value,
24+
onFocus: () => isSuppressing.value && update(),
25+
})
26+
},
27+
}), { props })
28+
}
29+
30+
it.each([
31+
[{ autocomplete: undefined, name: undefined }],
32+
[{ autocomplete: 'on', name: undefined }],
33+
[{ autocomplete: undefined, name: 'username' }],
34+
[{ autocomplete: 'current-password', name: 'password' }],
35+
[{ autocomplete: 'shipping street-address', name: 'shipping-address' }],
36+
[{ autocomplete: 'off', name: 'email' }],
37+
])('should pass through regular props', async (props: InputAutocompleteProps) => {
38+
const wrapper = mountFunction(props)
39+
40+
expect(wrapper.vm.$el.autocomplete).toEqual(props.autocomplete ?? '')
41+
expect(wrapper.vm.$el.name).toEqual(props.name ?? '')
42+
43+
wrapper.trigger('focus')
44+
await nextTick()
45+
expect(wrapper.vm.$el.name).toEqual(props.name ?? '')
46+
})
47+
48+
it('[suppress] should update field name on focus', async () => {
49+
const wrapper = mountFunction({ autocomplete: 'suppress', name: 'username' })
50+
51+
expect(wrapper.vm.$el.autocomplete).toBe('off')
52+
expect(wrapper.vm.$el.name).toBe('username-v-0-0')
53+
54+
wrapper.trigger('focus')
55+
await nextTick()
56+
expect(wrapper.vm.$el.name).toMatch(/username-v-0-\d{13}/)
57+
})
58+
})
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Utilities
2+
import { shallowRef, toRef, useId } from 'vue'
3+
import { propsFactory } from '@/util'
4+
5+
// Types
6+
import type { PropType } from 'vue'
7+
8+
// Types
9+
export interface InputAutocompleteProps {
10+
autocomplete: 'suppress' | string | undefined
11+
name?: string
12+
}
13+
14+
// Composables
15+
export const makeAutocompleteProps = propsFactory({
16+
autocomplete: String as PropType<'suppress' | string>,
17+
}, 'autocomplete')
18+
19+
export function useAutocomplete (props: InputAutocompleteProps) {
20+
const uniqueId = useId()
21+
const reloadTrigger = shallowRef(0)
22+
23+
const isSuppressing = toRef(() => props.autocomplete === 'suppress')
24+
25+
const fieldName = toRef(() => {
26+
return isSuppressing.value
27+
? `${props.name}-${uniqueId}-${reloadTrigger.value}`
28+
: props.name
29+
})
30+
31+
const fieldAutocomplete = toRef(() => {
32+
return isSuppressing.value
33+
? 'off'
34+
: props.autocomplete
35+
})
36+
37+
return {
38+
isSuppressing,
39+
fieldAutocomplete,
40+
fieldName,
41+
update: () => reloadTrigger.value = new Date().getTime(),
42+
}
43+
}

0 commit comments

Comments
 (0)