Skip to content

Commit 545d706

Browse files
authored
feat(select): add inputAttrs (#317)
1 parent 7ae6728 commit 545d706

File tree

6 files changed

+281
-6
lines changed

6 files changed

+281
-6
lines changed

docs/.vitepress/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ export default defineConfig({
5757
{ text: "Pre-selected values", link: "/demo/pre-selected-values" },
5858
],
5959
},
60+
{
61+
text: "Guides",
62+
items: [
63+
{ text: "Input attributes", link: "/guides/input-attributes" },
64+
],
65+
},
6066
],
6167

6268
socialLinks: [

docs/guides/input-attributes.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
---
2+
title: 'Input Attributes'
3+
---
4+
5+
# Input Attributes
6+
7+
Use the `inputAttrs` prop to add HTML attributes to the search input element. This is particularly useful for form integration, accessibility, and browser features.
8+
9+
## Autocomplete Support
10+
11+
Enable browser autocomplete by setting the `autocomplete` attribute:
12+
13+
```vue
14+
<template>
15+
<VueSelect
16+
v-model="selectedCountry"
17+
:options="countries"
18+
:input-attrs="{ autocomplete: 'country' }"
19+
placeholder="Select your country"
20+
/>
21+
22+
<VueSelect
23+
v-model="selectedUsername"
24+
:options="usernames"
25+
:input-attrs="{ autocomplete: 'username' }"
26+
placeholder="Select username"
27+
is-taggable
28+
/>
29+
</template>
30+
```
31+
32+
::: warning
33+
`autocomplete` browser UI might conflict with the component's UI. For example, if you are using the `autocomplete` attribute on a select field, the browser will show a dropdown of autocomplete options. To avoid this, you can set the `autocomplete` attribute to `off` (which is the default value).
34+
:::
35+
36+
## Required Field Validation
37+
38+
Mark fields as required for HTML5 form validation:
39+
40+
```vue
41+
<template>
42+
<form @submit.prevent="handleSubmit">
43+
<VueSelect
44+
v-model="selectedCountry"
45+
:options="countries"
46+
:input-attrs="{ required: true }"
47+
placeholder="Country (required)"
48+
/>
49+
50+
<button type="submit">
51+
Submit
52+
</button>
53+
</form>
54+
</template>
55+
```
56+
57+
::: info
58+
It isn't recommended to use HTML5 form validation with custom components, as you might run into issues. It's better instead to use a dedicated library for form validation (e.g. vee-validate) as it's easier to connect it with the component's data.
59+
:::
60+
61+
## Data Attributes
62+
63+
Add custom data attributes for testing or analytics:
64+
65+
```vue
66+
<template>
67+
<VueSelect
68+
v-model="selectedOption"
69+
:options="options"
70+
:input-attrs="{
71+
'data-testid': 'user-preference-select',
72+
'data-analytics': 'form-interaction',
73+
'data-component': 'country-selector',
74+
}"
75+
/>
76+
</template>
77+
```
78+
79+
## Overriding Default Attributes
80+
81+
The component sets some default attributes that you can override:
82+
83+
- `autocapitalize: "none"`
84+
- `autocomplete: "off"`
85+
- `autocorrect: "off"`
86+
- `spellcheck: false`
87+
- `tabindex: 0`
88+
- `type: "text"`
89+
90+
Essential attributes like `aria-autocomplete`, `aria-labelledby`, `disabled`, and `placeholder` are preserved and cannot be overridden through `inputAttrs`.
91+
92+
```vue
93+
<template>
94+
<!-- Override default autocomplete="off" -->
95+
<VueSelect
96+
v-model="username"
97+
:options="usernames"
98+
:input-attrs="{ autocomplete: 'username' }"
99+
/>
100+
101+
<!-- Enable spellcheck (default is false) -->
102+
<VueSelect
103+
v-model="comment"
104+
:options="comments"
105+
:input-attrs="{ spellcheck: true }"
106+
is-taggable
107+
/>
108+
</template>
109+
```

docs/props.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,48 @@ Top and left properties are calculated using a ref on the `.vue-select` with a `
187187

188188
The `id` attribute to be passed to the `<input />` element. This is useful for accessibility or forms.
189189

190+
## inputAttrs
191+
192+
**Type**: `Record<string, string | number | boolean | undefined | null | Array<unknown>>`
193+
194+
**Default**: `undefined`
195+
196+
HTML attributes to apply to the search input element. This is useful for form integration and accessibility.
197+
198+
Common use cases include:
199+
- `tabindex` - Control tab order in forms
200+
- `autocomplete` - Enable browser autocomplete (e.g., "country", "username")
201+
- `required` - Mark field as required for form validation
202+
- `data-*` - Custom data attributes for testing or analytics
203+
- Any other valid HTML input attributes
204+
205+
**Example:**
206+
207+
```vue
208+
<template>
209+
<VueSelect
210+
v-model="country"
211+
:options="countries"
212+
:input-attrs="{
213+
'tabindex': 2,
214+
'autocomplete': 'country',
215+
'required': true,
216+
'data-testid': 'country-select',
217+
}"
218+
/>
219+
</template>
220+
```
221+
222+
::: info
223+
User-provided attributes override the default attributes. Default attributes include:
224+
- `autocapitalize: "none"`
225+
- `autocomplete: "off"`
226+
- `autocorrect: "off"`
227+
- `spellcheck: false`
228+
- `tabindex: 0`
229+
- `type: "text"`
230+
:::
231+
190232
## classes
191233

192234
**Type**:

src/Select.spec.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,106 @@ describe("component props", () => {
389389
});
390390
});
391391

392+
describe("inputAttrs prop", () => {
393+
it("should apply custom tabindex from inputAttrs", () => {
394+
const wrapper = mount(VueSelect, {
395+
props: {
396+
modelValue: null,
397+
options,
398+
inputAttrs: { tabindex: 5 },
399+
},
400+
});
401+
402+
expect(wrapper.get("input").attributes("tabindex")).toBe("5");
403+
});
404+
405+
it("should apply custom autocomplete from inputAttrs", () => {
406+
const wrapper = mount(VueSelect, {
407+
props: {
408+
modelValue: null,
409+
options,
410+
inputAttrs: { autocomplete: "country" },
411+
},
412+
});
413+
414+
expect(wrapper.get("input").attributes("autocomplete")).toBe("country");
415+
});
416+
417+
it("should apply required attribute from inputAttrs", () => {
418+
const wrapper = mount(VueSelect, {
419+
props: {
420+
modelValue: null,
421+
options,
422+
inputAttrs: { required: true },
423+
},
424+
});
425+
426+
expect(wrapper.get("input").attributes("required")).toBe("");
427+
});
428+
429+
it("should apply multiple custom attributes from inputAttrs", () => {
430+
const wrapper = mount(VueSelect, {
431+
props: {
432+
modelValue: null,
433+
options,
434+
inputAttrs: {
435+
"tabindex": 3,
436+
"autocomplete": "username",
437+
"required": true,
438+
"data-testid": "custom-select",
439+
},
440+
},
441+
});
442+
443+
const input = wrapper.get("input");
444+
expect(input.attributes("tabindex")).toBe("3");
445+
expect(input.attributes("autocomplete")).toBe("username");
446+
expect(input.attributes("required")).toBe("");
447+
expect(input.attributes("data-testid")).toBe("custom-select");
448+
});
449+
450+
it("should override default attributes with inputAttrs", () => {
451+
const wrapper = mount(VueSelect, {
452+
props: {
453+
modelValue: null,
454+
options,
455+
inputAttrs: {
456+
spellcheck: true,
457+
autocorrect: "on",
458+
},
459+
},
460+
});
461+
462+
const input = wrapper.get("input");
463+
expect(input.attributes("spellcheck")).toBe("true");
464+
expect(input.attributes("autocorrect")).toBe("on");
465+
});
466+
467+
it("should preserve essential attributes when inputAttrs is provided", () => {
468+
const wrapper = mount(VueSelect, {
469+
props: {
470+
modelValue: null,
471+
options,
472+
inputAttrs: { tabindex: 5 },
473+
},
474+
});
475+
476+
const input = wrapper.get("input");
477+
expect(input.attributes("type")).toBe("text");
478+
expect(input.attributes("aria-autocomplete")).toBe("list");
479+
expect(input.attributes("placeholder")).toBe("");
480+
});
481+
482+
it("should work without inputAttrs prop", () => {
483+
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
484+
485+
const input = wrapper.get("input");
486+
expect(input.attributes("tabindex")).toBe("0");
487+
expect(input.attributes("autocomplete")).toBe("off");
488+
expect(input.attributes("spellcheck")).toBe("false");
489+
});
490+
});
491+
392492
describe("taggable prop", () => {
393493
it("should emit option-created event when pressing enter with search value", async () => {
394494
const wrapper = mount(VueSelect, { props: { modelValue: null, options, isTaggable: true } });

src/Select.vue

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const props = withDefaults(
3131
uid: uniqueId(),
3232
aria: undefined,
3333
disableInvalidVModelWarn: false,
34+
inputAttrs: undefined,
3435
filterBy: (option: GenericOption, label: string, search: string) => label.toLowerCase().includes(search.toLowerCase()),
3536
getOptionValue: (option: GenericOption) => option.value,
3637
getOptionLabel: (option: GenericOption) => option.label,
@@ -105,6 +106,22 @@ const selectedOptions = computed<GenericOption[]>(() => {
105106
return found ? [found] : [];
106107
});
107108
109+
const inputAttributes = computed(() => {
110+
const defaultAttrs = {
111+
autocapitalize: "none",
112+
autocomplete: "off",
113+
autocorrect: "off",
114+
spellcheck: false,
115+
tabindex: 0,
116+
type: "text",
117+
};
118+
119+
return {
120+
...defaultAttrs,
121+
...props.inputAttrs,
122+
};
123+
});
124+
108125
function openMenu() {
109126
if (props.isDisabled) {
110127
return;
@@ -388,12 +405,7 @@ watch(
388405
v-model="search"
389406
class="search-input"
390407
:class="props.classes?.searchInput"
391-
autocapitalize="none"
392-
autocomplete="off"
393-
autocorrect="off"
394-
spellcheck="false"
395-
tabindex="0"
396-
type="text"
408+
v-bind="inputAttributes"
397409
aria-autocomplete="list"
398410
:aria-labelledby="`vue-select-${uid}-combobox`"
399411
:disabled="isDisabled"

src/types/props.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,10 @@ export type Props<GenericOption, OptionValue> = {
149149
* @param option The option to render.
150150
*/
151151
getOptionLabel?: (option: GenericOption) => string;
152+
153+
/**
154+
* HTML attributes to apply to the search input element.
155+
* Useful for form integration (tabindex, autocomplete, required, etc.).
156+
*/
157+
inputAttrs?: Record<string, string | number | boolean | undefined | null | Array<unknown>>;
152158
};

0 commit comments

Comments
 (0)