@stackline/vue-multiselect-dropdown 3.0.3 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,31 +1,93 @@
1
1
  # @stackline/vue-multiselect-dropdown
2
2
 
3
- > A maintained Vue multiselect dropdown with separate Vue 2 and Vue 3 release lines, controlled state, searchable/grouped options, lazy loading hooks, render functions, skins, body-overlay positioning, and ADA-compliant keyboard/ARIA behavior.
3
+ > A maintained Vue multiselect dropdown with Vue 2 and Vue 3 release lines, controlled `v-model` state, scoped slots, renderless/state composables, searchable/grouped options, lazy loading hooks, custom render functions, skins, body-overlay positioning, and accessibility-focused and keyboard/ARIA tested behavior.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@stackline/vue-multiselect-dropdown.svg?style=flat-square)](https://www.npmjs.com/package/@stackline/vue-multiselect-dropdown)
6
+ [![npm monthly](https://img.shields.io/npm/dm/@stackline/vue-multiselect-dropdown.svg?style=flat-square)](https://www.npmjs.com/package/@stackline/vue-multiselect-dropdown)
6
7
  [![license](https://img.shields.io/npm/l/@stackline/vue-multiselect-dropdown.svg?style=flat-square)](https://github.com/alexandroit/vue-multiselect-dropdown/blob/main/LICENSE)
7
- [![Vue 2](https://img.shields.io/badge/Vue-2.x-42b883?style=flat-square)](https://alexandro.net/docs/vue/multiselect/vue-2/)
8
- [![Vue 3](https://img.shields.io/badge/Vue-3.x-42b883?style=flat-square)](https://alexandro.net/docs/vue/multiselect/vue-3/)
8
+ [![Vue 3](https://img.shields.io/badge/Vue-3.x-42b883?style=flat-square&logo=vue.js)](https://alexandro.net/docs/vue/multiselect/vue-3/)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org)
10
+ [![Reddit community](https://img.shields.io/badge/community-r%2FStackline-ff4500?style=flat-square&logo=reddit&logoColor=white)](https://www.reddit.com/r/Stackline/)
9
11
 
10
- **[Documentation & Live Demos](https://alexandro.net/docs/vue/multiselect/)** | **[Vue 2 Demo](https://alexandro.net/docs/vue/multiselect/vue-2/)** | **[Vue 3 Demo](https://alexandro.net/docs/vue/multiselect/vue-3/)** | **[npm](https://www.npmjs.com/package/@stackline/vue-multiselect-dropdown)** | **[Repository](https://github.com/alexandroit/vue-multiselect-dropdown)**
12
+ **[Documentation & Live Demos](https://alexandro.net/docs/vue/multiselect/)** | **[Vue 2 Demo](https://alexandro.net/docs/vue/multiselect/vue-2/)** | **[Vue 3 Demo](https://alexandro.net/docs/vue/multiselect/vue-3/)** | **[npm](https://www.npmjs.com/package/@stackline/vue-multiselect-dropdown)** | **[Issues](https://github.com/alexandroit/vue-multiselect-dropdown/issues)** | **[Repository](https://github.com/alexandroit/vue-multiselect-dropdown)** | **[Community Discussions](https://www.reddit.com/r/Stackline/)**
11
13
 
12
- **Latest Vue 3 package release:** `3.0.3` for Vue `3.x`
14
+ <p align="center">
15
+ <img src="https://alexandro.net/images/public/2026/06/dropdownlist.gif" alt="@stackline/vue-multiselect-dropdown live dropdown preview" width="420">
16
+ </p>
13
17
 
14
- **Maintained Vue 2 package release:** `2.0.0` for Vue `2.x`
18
+ **Latest Vue 3 release:** `3.1.1` for Vue `3.x`
19
+
20
+ **Maintained Vue 2 release:** `2.0.0` for Vue `2.x`
15
21
 
16
22
  ---
17
23
 
18
- ## Why this library?
24
+ > **Credits:** Current maintenance, Vue release-line stewardship, publishing, and documentation by [Alexandro Paixao Marques](https://github.com/alexandroit/vue-multiselect-dropdown).
19
25
 
20
- `@stackline/vue-multiselect-dropdown` provides maintained Vue 2 and Vue 3 multiselect release lines for applications that need predictable selection state, search, grouping, skins, keyboard support, and live tested examples.
26
+ ---
27
+
28
+ ## Why this library?
21
29
 
22
- The package follows a familiar Stackline settings contract while staying idiomatic for each Vue family: bind with `v-model`, pass `:data`, customize behavior through `:settings`, and listen for `@select`, `@de-select`, `@select-all`, `@de-select-all`, `@open`, and `@close`.
30
+ `@stackline/vue-multiselect-dropdown` provides maintained Vue 2 and Vue 3 multiselect release lines for applications that need predictable selection state, search, grouping, skins, keyboard support, custom Vue markup, and live tested examples.
31
+
32
+ The package follows the familiar Stackline settings contract while staying idiomatic for Vue: pass `:data`, bind selected values with `v-model`, customize behavior through `:settings`, and listen for Vue events such as `@select`, `@de-select`, `@select-all`, `@de-select-all`, `@open`, and `@close`.
33
+
34
+ The current stable Vue 3 release is `3.1.1`. It adds renderless composables, a state composable, scoped-slot customization, helper APIs, and a strengthened combobox contract while keeping the styled `<VueMultiselectDropdown />` component compatible with the existing visual contract.
35
+
36
+ ## Features
37
+
38
+ | Feature | Supported |
39
+ | :--- | :---: |
40
+ | Vue 2 and Vue 3 maintained release lines | Yes |
41
+ | Multi-select and single-select modes | Yes |
42
+ | Controlled `v-model` selection | Yes |
43
+ | Scoped slots for custom Vue HTML | Yes |
44
+ | Renderless `useMultiSelectDropdown` composable | Yes |
45
+ | State-only `useMultiSelectState` composable | Yes |
46
+ | Search and filter | Yes |
47
+ | Group by field or callback | Yes |
48
+ | Custom item render functions | Yes |
49
+ | Custom badge render functions | Yes |
50
+ | Lazy loading hooks | Yes |
51
+ | Add-new-item from search text | Yes |
52
+ | Instance methods for open, close, focus, select all, and clear | Yes |
53
+ | Built-in `classic`, `material`, `dark`, `custom`, and `brand` skins | Yes |
54
+ | Accessibility-focused and keyboard/ARIA tested navigation, focus states, and ARIA labels | Yes |
55
+ | Multiselect options expose both `aria-selected` and `aria-checked` | Yes |
56
+ | Backspace/Escape combobox contract | Yes |
57
+ | Selected object preservation across async data refreshes | Yes |
58
+ | Dialog and overflow-container support through `appendToBody` / `tagToBody` | Yes |
59
+ | Left-aligned, vertically centered placeholder and single-value text | Yes |
60
+ | Versioned docs builds per Vue line | Yes |
61
+
62
+ ## Table of Contents
63
+
64
+ 1. [Vue Version Compatibility](#vue-version-compatibility)
65
+ 2. [Installation](#installation)
66
+ 3. [Setup](#setup)
67
+ 4. [Styling and Skins](#styling-and-skins)
68
+ 5. [Basic Usage](#basic-usage)
69
+ 6. [Customization Paths](#customization-paths)
70
+ 7. [Typed Helper API](#typed-helper-api)
71
+ 8. [Scoped Slots](#scoped-slots)
72
+ 9. [Headless Usage](#headless-usage)
73
+ 10. [Combobox Contract](#combobox-contract)
74
+ 11. [Official Vue 3 Test Matrix](#official-vue-3-test-matrix)
75
+ 12. [Custom Render Functions](#custom-render-functions)
76
+ 13. [Forms and Controlled State](#forms-and-controlled-state)
77
+ 14. [Lazy Loading and Dynamic Data](#lazy-loading-and-dynamic-data)
78
+ 15. [Dialogs and Overflow Containers](#dialogs-and-overflow-containers)
79
+ 16. [Events](#events)
80
+ 17. [Instance Methods](#instance-methods)
81
+ 18. [Run Locally](#run-locally)
82
+ 19. [License](#license)
23
83
 
24
84
  ## Vue Version Compatibility
25
85
 
26
- | Package family | Vue family | Peer range | First tested runtime | Demo link |
86
+ Each package family installs on its matching Vue family. Keep the package family aligned with the Vue major used by your application.
87
+
88
+ | Package family | Vue family | Peer range | Tested release window | Demo link |
27
89
  | :---: | :---: | :---: | :---: | :--- |
28
- | **3.x** | **Vue 3 only** | **`>=3.0.0 <4.0.0`** | **3.0.0** | [Vue 3 family docs](https://alexandro.net/docs/vue/multiselect/vue-3/) |
90
+ | **3.x** | **Vue 3 only** | **`>=3.0.0 <4.0.0`** | **3.0.0 through 3.5.35** | [Vue 3 family docs](https://alexandro.net/docs/vue/multiselect/vue-3/) |
29
91
  | **2.x** | **Vue 2 only** | **`>=2.0.0 <3.0.0`** | **2.0.0** | [Vue 2 family docs](https://alexandro.net/docs/vue/multiselect/vue-2/) |
30
92
 
31
93
  ## Installation
@@ -33,7 +95,7 @@ The package follows a familiar Stackline settings contract while staying idiomat
33
95
  For Vue 3 applications:
34
96
 
35
97
  ```bash
36
- npm install @stackline/vue-multiselect-dropdown@3.0.3 --save-exact
98
+ npm install @stackline/vue-multiselect-dropdown@3.1.1 --save-exact
37
99
  ```
38
100
 
39
101
  For Vue 2 applications:
@@ -42,74 +104,574 @@ For Vue 2 applications:
42
104
  npm install @stackline/vue-multiselect-dropdown@2.0.0 --save-exact
43
105
  ```
44
106
 
107
+ The styled component injects its component styles at runtime. The renderless composables do not require the built-in DOM and let your application own the markup and styling.
108
+
45
109
  ## Setup
46
110
 
111
+ ### 1. Import the component
112
+
47
113
  ```js
48
114
  import { createApp } from 'vue';
49
115
  import {
50
116
  VueMultiselect,
51
117
  VueMultiselectDropdown
52
118
  } from '@stackline/vue-multiselect-dropdown';
119
+ ```
53
120
 
54
- const app = createApp({
55
- components: { VueMultiselectDropdown },
56
- data() {
57
- return {
58
- countries: [
59
- { id: 1, itemName: 'Brazil' },
60
- { id: 2, itemName: 'Canada' },
61
- { id: 3, itemName: 'Portugal' }
62
- ],
63
- selectedCountries: [],
64
- settings: {
65
- text: 'Select countries',
66
- enableSearchFilter: true,
67
- primaryKey: 'id',
68
- labelKey: 'itemName',
69
- badgeShowLimit: 3,
70
- skin: 'classic'
71
- }
72
- };
73
- }
74
- });
121
+ ### 2. Keep selection in Vue state
122
+
123
+ ```js
124
+ const selectedCountries = ref([]);
125
+ ```
126
+
127
+ ### 3. Pass a stable settings object
128
+
129
+ ```js
130
+ const settings = {
131
+ text: 'Select countries',
132
+ enableSearchFilter: true,
133
+ primaryKey: 'id',
134
+ labelKey: 'itemName',
135
+ badgeShowLimit: 3,
136
+ skin: 'classic'
137
+ };
138
+ ```
139
+
140
+ Register globally:
141
+
142
+ ```js
143
+ const app = createApp(App);
75
144
 
76
145
  app.use(VueMultiselect);
77
146
  app.mount('#app');
78
147
  ```
79
148
 
80
- ```html
81
- <vue-multiselect-dropdown
82
- :data="countries"
83
- v-model="selectedCountries"
84
- :settings="settings"
85
- />
149
+ Or register locally:
150
+
151
+ ```js
152
+ export default {
153
+ components: { VueMultiselectDropdown }
154
+ };
155
+ ```
156
+
157
+ ## Styling and Skins
158
+
159
+ Use `settings.skin` to switch the visual mode:
160
+
161
+ ```js
162
+ settings.skin = 'material';
163
+ ```
164
+
165
+ Built-in skins:
166
+
167
+ | Skin | Usage |
168
+ | :--- | :--- |
169
+ | `classic` | Compact classic dropdown styling. |
170
+ | `material` | Material-style rounded controls and chips. |
171
+ | `dark` | Dark UI surfaces. |
172
+ | `custom` | CSS-variable starter skin for custom projects. |
173
+ | `brand` | Stackline brand skin. |
174
+
175
+ `settings.theme` is accepted as a legacy alias, but new Vue usage should configure only `settings.skin`.
176
+
177
+ ## Basic Usage
178
+
179
+ ```vue
180
+ <script setup>
181
+ import { ref } from 'vue';
182
+ import {
183
+ VueMultiselectDropdown,
184
+ defineSettings
185
+ } from '@stackline/vue-multiselect-dropdown';
186
+
187
+ const countries = [
188
+ { id: 1, itemName: 'Brazil', capital: 'Brasilia', region: 'South America' },
189
+ { id: 2, itemName: 'Canada', capital: 'Ottawa', region: 'North America' },
190
+ { id: 3, itemName: 'Portugal', capital: 'Lisbon', region: 'Europe' },
191
+ { id: 4, itemName: 'United States', capital: 'Washington, DC', region: 'North America' },
192
+ { id: 5, itemName: 'Argentina', capital: 'Buenos Aires', region: 'South America' },
193
+ { id: 6, itemName: 'Mexico', capital: 'Mexico City', region: 'North America' }
194
+ ];
195
+
196
+ const selectedCountries = ref([countries[1]]);
197
+
198
+ const settings = defineSettings({
199
+ singleSelection: false,
200
+ text: 'Select countries',
201
+ selectAllText: 'Select all',
202
+ unSelectAllText: 'Clear all',
203
+ enableSearchFilter: true,
204
+ searchPlaceholderText: 'Search',
205
+ primaryKey: 'id',
206
+ labelKey: 'itemName',
207
+ badgeShowLimit: 4,
208
+ maxHeight: 260,
209
+ showCheckbox: true,
210
+ noDataLabel: 'No data',
211
+ skin: 'classic',
212
+ appendToBody: false
213
+ });
214
+
215
+ function handleSelect(item) {
216
+ console.log('selected', item);
217
+ }
218
+
219
+ function handleRemove(item) {
220
+ console.log('removed', item);
221
+ }
222
+ </script>
223
+
224
+ <template>
225
+ <VueMultiselectDropdown
226
+ :data="countries"
227
+ v-model="selectedCountries"
228
+ :settings="settings"
229
+ @select="handleSelect"
230
+ @de-select="handleRemove"
231
+ @select-all="(items) => console.log('selected all', items)"
232
+ @de-select-all="(items) => console.log('cleared all', items)"
233
+ />
234
+ </template>
235
+ ```
236
+
237
+ ## Customization Paths
238
+
239
+ Use the API layer that matches the amount of control your team needs:
240
+
241
+ | Layer | Best for | What you own |
242
+ | :--- | :--- | :--- |
243
+ | `<VueMultiselectDropdown />` | Fast forms, filters, dashboards, and admin screens. | Data, selected state, settings, events, and optional render functions. |
244
+ | `createVueMultiselectDropdown<T>()` | Teams that want a typed helper around settings and composables. | Typed state/composable usage for a feature or design-system wrapper. |
245
+ | Scoped slots | Custom Vue HTML around the proven component behavior. | Specific DOM pieces such as trigger, badges, menu footer, group headers, and options. |
246
+ | `useMultiSelectDropdown` | Fully custom interfaces and design systems. | All markup and CSS, while Stackline provides state, ARIA prop bags, keyboard flow, grouping, and callbacks. |
247
+ | `useMultiSelectState` | Advanced state engines or existing combobox shells. | Every element, every event binding, and all visual behavior. |
248
+
249
+ For most teams, start with the component. Use scoped slots when the component works but your layout needs a different shell. Use the renderless composables when the application must own the whole combobox contract.
250
+
251
+ ## Typed Helper API
252
+
253
+ Use `createVueMultiselectDropdown<T>()` when a feature, package, or design-system wrapper should bind the item type once and reuse it across settings and composables.
254
+
255
+ This is optional. The normal `<VueMultiselectDropdown />` API remains the fastest path for most screens.
256
+
257
+ ```ts
258
+ import {
259
+ createVueMultiselectDropdown
260
+ } from '@stackline/vue-multiselect-dropdown';
261
+
262
+ type Country = {
263
+ id: number;
264
+ itemName: string;
265
+ capital: string;
266
+ region: string;
267
+ };
268
+
269
+ const CountryMultiselect = createVueMultiselectDropdown<Country>();
270
+
271
+ const countrySettings = CountryMultiselect.defineSettings({
272
+ text: 'Choose countries',
273
+ primaryKey: 'id',
274
+ labelKey: 'itemName',
275
+ searchBy: ['itemName', 'capital'],
276
+ groupBy: 'region',
277
+ enableSearchFilter: true,
278
+ badgeShowLimit: 2
279
+ });
280
+
281
+ export function useCountryMultiselect(countries, selectedCountries) {
282
+ return CountryMultiselect.useMultiSelectDropdown({
283
+ data: countries,
284
+ selectedItems: selectedCountries,
285
+ settings: countrySettings,
286
+ onChange(items) {
287
+ selectedCountries.value = items;
288
+ }
289
+ });
290
+ }
86
291
  ```
87
292
 
88
- ## Skins
293
+ The helper is useful when a design-system wrapper wants one typed place for settings, state, and renderless dropdown behavior. The styled Vue component remains available for normal template usage.
294
+
295
+ ## Scoped Slots
296
+
297
+ Scoped slots let you replace the visible Vue HTML pieces while the package still owns the tested selection, filtering, keyboard, focus, ARIA, body overlay, and async behavior.
89
298
 
90
- Use `settings.skin` for `classic`, `material`, `dark`, `custom`, and `brand`.
299
+ The important rule is simple: keep the provided slot callbacks and ARIA values connected to your custom elements. They carry the behavior that makes the component accessible.
91
300
 
92
- `settings.theme` is accepted only as a compatibility alias. Prefer `settings.skin` in new code.
301
+ ```vue
302
+ <script setup>
303
+ import { ref } from 'vue';
304
+ import { VueMultiselectDropdown } from '@stackline/vue-multiselect-dropdown';
305
+
306
+ const selectedCountries = ref([]);
307
+
308
+ const countries = [
309
+ { id: 1, itemName: 'Brazil', capital: 'Brasilia', region: 'South America' },
310
+ { id: 2, itemName: 'Canada', capital: 'Ottawa', region: 'North America' },
311
+ { id: 3, itemName: 'Portugal', capital: 'Lisbon', region: 'Europe' }
312
+ ];
313
+
314
+ const settings = {
315
+ text: 'Choose countries',
316
+ enableSearchFilter: true,
317
+ groupBy: 'region',
318
+ primaryKey: 'id',
319
+ labelKey: 'itemName',
320
+ badgeShowLimit: 2,
321
+ skin: 'classic'
322
+ };
323
+ </script>
324
+
325
+ <template>
326
+ <VueMultiselectDropdown
327
+ :data="countries"
328
+ v-model="selectedCountries"
329
+ :settings="settings"
330
+ >
331
+ <template #option="{ item, label, selected, ariaSelected, ariaChecked, toggle }">
332
+ <button
333
+ type="button"
334
+ class="country-option"
335
+ :aria-pressed="selected"
336
+ @click.stop="toggle"
337
+ >
338
+ <span>
339
+ <strong>{{ label }}</strong>
340
+ <small>{{ item.capital }} - {{ item.region }}</small>
341
+ </span>
342
+ <code>{{ ariaSelected }}/{{ ariaChecked }}</code>
343
+ </button>
344
+ </template>
345
+
346
+ <template #badge="{ label, remove }">
347
+ <span class="country-badge">
348
+ {{ label }}
349
+ <button type="button" @click.stop="remove">Remove</button>
350
+ </span>
351
+ </template>
352
+
353
+ <template #menu-footer="{ selected, filteredItems }">
354
+ <div class="country-footer">
355
+ {{ selected.length }} selected - {{ filteredItems.length }} visible
356
+ </div>
357
+ </template>
358
+ </VueMultiselectDropdown>
359
+ </template>
360
+ ```
361
+
362
+ Available scoped slots:
363
+
364
+ `trigger`, `placeholder`, `badge`, `option`, `loading`, `empty`, `group-header`, and `menu-footer`.
365
+
366
+ ## Headless Usage
367
+
368
+ Use `useMultiSelectDropdown` when you want Stackline selection, filtering, keyboard handling, ARIA prop bags, grouping, limits, and callbacks without the built-in DOM/CSS.
369
+
370
+ The flag sample below uses SVG country icons from `flag-icons`. You can replace that import with your own icon system if your app already has one.
371
+
372
+ ```bash
373
+ npm install flag-icons
374
+ ```
375
+
376
+ ```vue
377
+ <script setup>
378
+ import { computed, ref } from 'vue';
379
+ import 'flag-icons/css/flag-icons.min.css';
380
+ import { useMultiSelectDropdown } from '@stackline/vue-multiselect-dropdown';
381
+
382
+ const countries = [
383
+ { id: 1, itemName: 'Brazil', flag: 'BR', capital: 'Brasilia', region: 'Americas' },
384
+ { id: 2, itemName: 'Canada', flag: 'CA', capital: 'Ottawa', region: 'Americas' },
385
+ { id: 3, itemName: 'Portugal', flag: 'PT', capital: 'Lisbon', region: 'Europe' }
386
+ ];
387
+
388
+ const selectedItems = ref([countries[0]]);
389
+
390
+ const dropdown = useMultiSelectDropdown({
391
+ data: countries,
392
+ selectedItems,
393
+ onChange(items) {
394
+ selectedItems.value = items;
395
+ },
396
+ settings: {
397
+ text: 'Choose countries',
398
+ enableSearchFilter: true,
399
+ searchPlaceholderText: 'Search country',
400
+ groupBy: 'region',
401
+ primaryKey: 'id',
402
+ labelKey: 'itemName',
403
+ badgeShowLimit: 2,
404
+ clearAll: true
405
+ }
406
+ });
407
+
408
+ const selectedSummary = computed(() => (
409
+ dropdown.selectedItems.value.length
410
+ ? `${dropdown.selectedItems.value.length} selected`
411
+ : 'No selected countries'
412
+ ));
413
+
414
+ function flagClass(code) {
415
+ return `fi fi-${code.toLowerCase()}`;
416
+ }
417
+ </script>
418
+
419
+ <template>
420
+ <div class="country-picker" v-bind="dropdown.getRootProps()">
421
+ <button class="country-trigger" v-bind="dropdown.getTriggerProps()">
422
+ <span>{{ dropdown.label.value }}</span>
423
+ <strong>{{ dropdown.isOpen.value ? 'Close' : 'Open' }}</strong>
424
+ </button>
425
+
426
+ <div class="country-chips" :aria-label="selectedSummary">
427
+ <span
428
+ v-for="item in dropdown.selectedItems.value"
429
+ :key="dropdown.getItemKey(item)"
430
+ class="country-chip"
431
+ >
432
+ <span :class="['country-flag', flagClass(item.flag)]" aria-hidden="true" />
433
+ {{ dropdown.getItemLabel(item) }}
434
+ <button v-bind="dropdown.getRemoveButtonProps(item)">x</button>
435
+ </span>
436
+ </div>
437
+
438
+ <div
439
+ v-if="dropdown.isOpen.value"
440
+ class="country-panel"
441
+ v-bind="dropdown.getListboxProps()"
442
+ >
443
+ <input class="country-search" v-bind="dropdown.getSearchInputProps()" />
444
+
445
+ <div
446
+ v-for="option in dropdown.visibleOptions.value"
447
+ :key="option.key"
448
+ v-bind="dropdown.getOptionProps(option, {
449
+ class: option.selected ? 'country-option selected' : 'country-option'
450
+ })"
451
+ >
452
+ <span :class="['country-flag', flagClass(option.item.flag)]" aria-hidden="true" />
453
+ <span>
454
+ <strong>{{ option.label }}</strong>
455
+ <small>{{ option.item.capital }} - {{ option.item.region }}</small>
456
+ </span>
457
+ <input type="checkbox" :checked="option.selected" readonly />
458
+ </div>
459
+ </div>
460
+ </div>
461
+ </template>
462
+ ```
463
+
464
+ Use `useMultiSelectState` when you want the selection/filter/grouping engine without prop bags:
465
+
466
+ ```js
467
+ import { useMultiSelectState } from '@stackline/vue-multiselect-dropdown';
468
+
469
+ const state = useMultiSelectState({
470
+ data: countries,
471
+ selectedItems,
472
+ onChange(items) {
473
+ selectedItems.value = items;
474
+ },
475
+ settings: {
476
+ primaryKey: 'id',
477
+ labelKey: 'itemName',
478
+ enableSearchFilter: true
479
+ }
480
+ });
481
+ ```
482
+
483
+ The styled component remains available for drop-in usage. The renderless composables are for teams that want a headless-style ownership model where the application controls layout, elements, and CSS.
484
+
485
+ ## Combobox Contract
486
+
487
+ Version `3.1.1` tightens the interaction details that usually matter most in production forms:
488
+
489
+ | Behavior | Contract |
490
+ | :--- | :--- |
491
+ | Focus after selection/removal | Focus returns to search while the list stays open, or to the trigger when the list closes. |
492
+ | Option selection state | Multiselect options expose matching `aria-selected` and `aria-checked` values. |
493
+ | Backspace in search | Edits the query. With an empty query it does not remove selected values by default. |
494
+ | Backspace/Delete on focused badge remove button | Removes that selected badge. |
495
+ | Escape | Closes the list without clearing selected values. |
496
+ | Async option refresh | Selected object values stay stable by `primaryKey` when data refreshes. |
497
+ | Keyboard navigation | Trigger ArrowDown/ArrowUp, option Home/End, and option ids keep focus and ARIA predictable. |
498
+
499
+ Keyboard behavior is enabled by default. You can turn each part off from `settings.keyboard` when an application needs a stricter interaction model:
500
+
501
+ ```js
502
+ const settings = {
503
+ text: 'Countries',
504
+ enableSearchFilter: true,
505
+ keyboard: {
506
+ space: true,
507
+ spaceOptionAction: 'toggle',
508
+ tab: true,
509
+ arrows: true,
510
+ escape: true,
511
+ backspaceRemovesLastWhenSearchEmpty: false,
512
+ deleteRemovesFocusedBadge: true
513
+ }
514
+ };
515
+ ```
516
+
517
+ Set any key to `false` to disable that behavior. `backspaceRemovesLastWhenSearchEmpty` can be turned on for applications that want the legacy "empty search removes last badge" pattern. `keyboard.backspace` is still accepted as a deprecated alias for that legacy behavior. `spaceOptionAction` controls only focused options:
518
+ `'toggle'` keeps focus on the current option, while `'toggle-and-next'` toggles and moves to the next enabled option.
519
+ `escapeToClose: false` is still supported and also disables `keyboard.escape`.
520
+
521
+ ## Official Vue 3 Test Matrix
522
+
523
+ The Vue 3 release was tested in a clean Vue `3.5.35` application with `@stackline/vue-multiselect-dropdown@3.1.1`. The docs use the same examples from that test app, including keyboard navigation, focus, ARIA behavior, badge counters, responsive action buttons, scrollable lists, dialog-safe body overlays, the corrected left-aligned placeholder with vertical centering, scoped-slot customization, headless/custom HTML, and the combobox contract checks for Backspace, Escape, focused badge removal, focus, and option ARIA.
524
+
525
+ The same core scenarios are validated for the visual skins:
526
+
527
+ | # | Scenario | Main settings tested |
528
+ | :---: | :--- | :--- |
529
+ | 01 | Basic multi | `{ enableSearchFilter: true }` |
530
+ | 02 | All selected badges visible | `{ badgeShowLimit: 10 }` |
531
+ | 03 | Single selection | `{ singleSelection: true }` |
532
+ | 04 | Search by fields | `{ searchBy: ['itemName', 'capital'] }` |
533
+ | 05 | Grouped options | `{ groupBy: 'category', selectGroup: true }` |
534
+ | 06 | Selection limit | `{ limitSelection: 2, badgeShowLimit: 2 }` |
535
+ | 07 | Custom rendering | `renderItem` and `renderBadge` |
536
+ | 08 | Search and add item | `{ addNewItemOnFilter: true }` |
537
+ | 09 | Disabled state | `{ disabled: true }` |
538
+ | 10 | Controlled form validation | Vue state and derived validation |
539
+ | 11 | Long list with keyboard scroll | `{ maxHeight: 140 }` |
540
+ | 12 | Local lazy loading | `{ lazyLoading: true }` |
541
+ | 13 | Dialog and overflow container | `{ appendToBody: true, tagToBody: true }` |
542
+ | 14 | Body overlay auto direction | `{ autoPosition: true, position: 'top' }` |
543
+ | 15 | Instance methods | `openDropdown`, `closeDropdown`, `selectAll`, `clearSelection` |
544
+ | 16 | Scoped slots custom HTML | `#trigger`, `#option`, `#badge`, `#group-header`, `#menu-footer` |
545
+
546
+ ## Custom Render Functions
547
+
548
+ Use `renderItem` for option rows and `renderBadge` for selected chips when you only need to replace inner content. Use scoped slots when you need to replace component structure.
549
+
550
+ ```vue
551
+ <script setup>
552
+ import { h, ref } from 'vue';
553
+ import { VueMultiselectDropdown } from '@stackline/vue-multiselect-dropdown';
554
+
555
+ const selectedCountries = ref([]);
556
+
557
+ function renderItem(item, context) {
558
+ return h('span', [
559
+ h('strong', context.label),
560
+ h('small', item.capital)
561
+ ]);
562
+ }
563
+
564
+ function renderBadge(item) {
565
+ return h('span', item.itemName);
566
+ }
567
+ </script>
568
+
569
+ <template>
570
+ <VueMultiselectDropdown
571
+ :data="countries"
572
+ v-model="selectedCountries"
573
+ :settings="settings"
574
+ :render-item="renderItem"
575
+ :render-badge="renderBadge"
576
+ />
577
+ </template>
578
+ ```
579
+
580
+ ## Forms and Controlled State
581
+
582
+ Keep the selected array in Vue state and derive validity from that state:
583
+
584
+ ```vue
585
+ <script setup>
586
+ import { computed, ref } from 'vue';
587
+ import { VueMultiselectDropdown } from '@stackline/vue-multiselect-dropdown';
588
+
589
+ const name = ref('');
590
+ const email = ref('');
591
+ const selectedSkills = ref([]);
592
+
593
+ const formIsValid = computed(() => (
594
+ email.value.trim().length > 0 && selectedSkills.value.length > 0
595
+ ));
596
+ </script>
597
+
598
+ <template>
599
+ <form @submit.prevent>
600
+ <input v-model="name" />
601
+ <input v-model="email" />
602
+
603
+ <VueMultiselectDropdown
604
+ :data="skills"
605
+ v-model="selectedSkills"
606
+ :settings="skillSettings"
607
+ />
608
+
609
+ <button type="submit" :disabled="!formIsValid">
610
+ Submit
611
+ </button>
612
+ </form>
613
+ </template>
614
+ ```
615
+
616
+ ## Lazy Loading and Dynamic Data
617
+
618
+ Enable lazy loading through the settings object and append more rows when the list reaches the end:
619
+
620
+ ```vue
621
+ <script setup>
622
+ import { ref } from 'vue';
623
+ import { VueMultiselectDropdown } from '@stackline/vue-multiselect-dropdown';
624
+
625
+ const people = ref(loadFirstPage());
626
+ const selectedPeople = ref([]);
627
+
628
+ const settings = {
629
+ text: 'Select people',
630
+ enableSearchFilter: true,
631
+ lazyLoading: true,
632
+ labelKey: 'name',
633
+ primaryKey: 'id',
634
+ maxHeight: 140
635
+ };
636
+
637
+ function appendPeople() {
638
+ people.value = people.value.concat(loadMorePeople());
639
+ }
640
+ </script>
641
+
642
+ <template>
643
+ <VueMultiselectDropdown
644
+ :data="people"
645
+ v-model="selectedPeople"
646
+ :settings="settings"
647
+ @scroll-to-end="appendPeople"
648
+ />
649
+ </template>
650
+ ```
93
651
 
94
652
  ## Dialogs and Overflow Containers
95
653
 
96
654
  Use `appendToBody: true` or `tagToBody: true` when the dropdown is inside dialogs, modals, drawers, or containers that set `overflow: hidden` or `overflow: auto`.
97
655
 
98
656
  ```js
99
- settings: {
657
+ const settings = {
100
658
  text: 'Dialog dropdown',
101
659
  enableSearchFilter: true,
102
660
  skin: 'material',
103
661
  appendToBody: true,
104
662
  tagToBody: true,
105
663
  autoPosition: true
106
- }
664
+ };
107
665
  ```
108
666
 
109
- With body overlay enabled, the open panel is moved to `document.body`, aligned to the original trigger, sized to the trigger, recalculated on open, scroll, resize, and selection changes, then restored and cleaned up when the dropdown closes or unmounts.
667
+ With body overlay enabled, the open panel is rendered against `document.body`, aligned to the original trigger, sized to the trigger, recalculated on open, scroll, resize, and content changes, and cleaned up when the dropdown closes or the component unmounts.
668
+
669
+ `autoPosition: true` treats `position` as a preferred direction. The menu opens upward only when there is meaningfully less room below and enough room above; otherwise it opens below and shrinks the scrollable list height to stay visible without covering the trigger.
110
670
 
111
671
  ## Events
112
672
 
673
+ Available Vue events:
674
+
113
675
  - `input`
114
676
  - `change`
115
677
  - `select`
@@ -125,14 +687,42 @@ With body overlay enabled, the open panel is moved to `document.body`, aligned t
125
687
 
126
688
  ## Instance Methods
127
689
 
128
- ```js
129
- this.$refs.dropdown.openDropdown();
130
- this.$refs.dropdown.closeDropdown();
131
- this.$refs.dropdown.focusSearch();
132
- this.$refs.dropdown.selectAll();
133
- this.$refs.dropdown.clearSelection();
690
+ ```vue
691
+ <script setup>
692
+ import { ref } from 'vue';
693
+ import { VueMultiselectDropdown } from '@stackline/vue-multiselect-dropdown';
694
+
695
+ const dropdownRef = ref(null);
696
+
697
+ function openDropdown() {
698
+ dropdownRef.value?.openDropdown();
699
+ }
700
+
701
+ function clearSelection() {
702
+ dropdownRef.value?.clearSelection();
703
+ }
704
+ </script>
705
+
706
+ <template>
707
+ <VueMultiselectDropdown
708
+ ref="dropdownRef"
709
+ :data="countries"
710
+ v-model="selectedCountries"
711
+ :settings="settings"
712
+ />
713
+ </template>
134
714
  ```
135
715
 
716
+ Available methods:
717
+
718
+ - `openDropdown()`
719
+ - `closeDropdown()`
720
+ - `focusSearch()`
721
+ - `selectAll()`
722
+ - `deSelectAll()`
723
+ - `clearSelection()`
724
+ - `getSelectedItems()`
725
+
136
726
  ## Run Locally
137
727
 
138
728
  ```bash