@stackline/react-multiselect-dropdown 17.0.0 → 17.0.2

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,18 +1,21 @@
1
1
  # @stackline/react-multiselect-dropdown
2
2
 
3
- > A maintained React multiselect dropdown for React 17 applications, with controlled React state, searchable/grouped options, lazy loading hooks, custom render functions, skins, body-overlay positioning, and ADA-friendly keyboard/ARIA behavior.
3
+ > A maintained React multiselect dropdown for React 19 applications, with controlled React state, custom slots, headless/state hooks, 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/react-multiselect-dropdown.svg?style=flat-square)](https://www.npmjs.com/package/@stackline/react-multiselect-dropdown)
6
- [![npm downloads](https://img.shields.io/npm/dt/@stackline/react-multiselect-dropdown.svg?style=flat-square)](https://www.npmjs.com/package/@stackline/react-multiselect-dropdown)
7
6
  [![npm monthly](https://img.shields.io/npm/dm/@stackline/react-multiselect-dropdown.svg?style=flat-square)](https://www.npmjs.com/package/@stackline/react-multiselect-dropdown)
8
7
  [![license](https://img.shields.io/npm/l/@stackline/react-multiselect-dropdown.svg?style=flat-square)](https://github.com/alexandroit/react-multiselect-dropdown/blob/main/LICENSE)
9
- [![React 17](https://img.shields.io/badge/React-17.x-61dafb?style=flat-square&logo=react)](https://alexandro.net/docs/react/multiselect/react-17/)
8
+ [![React 19](https://img.shields.io/badge/React-19.x-61dafb?style=flat-square&logo=react)](https://alexandro.net/docs/react/multiselect/react-19/)
10
9
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org)
11
- [![GitHub stars](https://img.shields.io/github/stars/alexandroit/react-multiselect-dropdown.svg?style=flat-square)](https://github.com/alexandroit/react-multiselect-dropdown/stargazers)
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/)
12
11
 
13
- **[Documentation & Live Demos](https://alexandro.net/docs/react/multiselect/)** | **[React 17 Demo](https://alexandro.net/docs/react/multiselect/react-17/)** | **[npm](https://www.npmjs.com/package/@stackline/react-multiselect-dropdown)** | **[Issues](https://github.com/alexandroit/react-multiselect-dropdown/issues)** | **[Repository](https://github.com/alexandroit/react-multiselect-dropdown)**
12
+ **[Documentation & Live Demos](https://alexandro.net/docs/react/multiselect/)** | **[React 19 Demo](https://alexandro.net/docs/react/multiselect/react-19/)** | **[React 19 StackBlitz](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fbasic%2Fbasic.component.tsx&startScript=start&initialpath=%2Fbasic)** | **[npm](https://www.npmjs.com/package/@stackline/react-multiselect-dropdown)** | **[Issues](https://github.com/alexandroit/react-multiselect-dropdown/issues)** | **[Repository](https://github.com/alexandroit/react-multiselect-dropdown)** | **[Community Discussions](https://www.reddit.com/r/Stackline/)**
14
13
 
15
- **Latest tested package release:** `17.0.0` for React `17.x`
14
+ <p align="center">
15
+ <img src="https://alexandro.net/images/public/2026/06/dropdownlist.gif" alt="@stackline/react-multiselect-dropdown live dropdown preview" width="420">
16
+ </p>
17
+
18
+ **Latest React 19 release:** `19.1.3` for React `19.x`
16
19
 
17
20
  ---
18
21
 
@@ -24,17 +27,20 @@
24
27
 
25
28
  `@stackline/react-multiselect-dropdown` provides a maintained React multiselect component for applications that need predictable selection state, search, grouping, skins, keyboard support, and live tested examples.
26
29
 
27
- The package is built around a controlled React API: pass `data`, bind `selectedItems`, receive updates through `onChange`, and customize behavior through a `settings` object. It also supports custom React render functions for option rows and selected badges, lazy loading callbacks, imperative `ref` methods, and body-overlay positioning for dialogs or clipped containers.
30
+ The package is built around a controlled React API: pass `data`, bind `selectedItems`, receive updates through `onChange`, and customize behavior through a `settings` object. It also supports a Slots API for replacing component structure without losing ARIA/focus behavior, a headless `useMultiSelectDropdown` hook, a lower-level `useMultiSelectState` hook, custom React render functions for option rows and selected badges, lazy loading callbacks, imperative `ref` methods, and body-overlay positioning for dialogs or clipped containers.
28
31
 
29
- The current package release is `17.0.0` for React 17.x applications. It was tested in a clean React `17.0.2` application before publication to the local validation registry.
32
+ The current stable React 19 release is `19.1.3`. It adds guided structural slots, a headless hook, a state hook, a type-safe factory helper, and a strengthened combobox contract while keeping the styled `<MultiSelectDropdown />` component compatible with the existing visual contract.
30
33
 
31
34
  ## Features
32
35
 
33
36
  | Feature | Supported |
34
37
  | :--- | :---: |
35
- | React 17 tested published release line | Yes |
38
+ | React 19 tested release line | Yes |
36
39
  | Multi-select and single-select modes | Yes |
37
40
  | Controlled and uncontrolled selection | Yes |
41
+ | Guided Slots API for custom HTML structure | Yes |
42
+ | Headless `useMultiSelectDropdown` hook | Yes |
43
+ | State-only `useMultiSelectState` hook | Yes |
38
44
  | Search and filter | Yes |
39
45
  | Group by field | Yes |
40
46
  | Custom item render functions | Yes |
@@ -43,8 +49,13 @@ The current package release is `17.0.0` for React 17.x applications. It was test
43
49
  | Add-new-item from search text | Yes |
44
50
  | Ref methods for open, close, focus, select all, and clear | Yes |
45
51
  | Built-in `classic`, `material`, `dark`, `custom`, and `brand` skins | Yes |
46
- | ADA-friendly keyboard navigation, focus states, and ARIA labels | Yes |
52
+ | Accessibility-focused and keyboard/ARIA tested navigation, focus states, and ARIA labels | Yes |
53
+ | Multiselect options expose both `aria-selected` and `aria-checked` | Yes |
54
+ | Backspace/Escape combobox contract | Yes |
55
+ | Async add-item stale response protection | Yes |
56
+ | Selected object preservation across async data refreshes | Yes |
47
57
  | Dialog and overflow-container support through `appendToBody` / `tagToBody` | Yes |
58
+ | Left-aligned, vertically centered placeholder and single-value text | Yes |
48
59
  | Versioned docs builds per React line | Yes |
49
60
 
50
61
  ## Table of Contents
@@ -54,15 +65,20 @@ The current package release is `17.0.0` for React 17.x applications. It was test
54
65
  3. [Setup](#setup)
55
66
  4. [Styling and Skins](#styling-and-skins)
56
67
  5. [Basic Usage](#basic-usage)
57
- 6. [Official React 17 Test Matrix](#official-react-17-test-matrix)
58
- 7. [Custom Render Functions](#custom-render-functions)
59
- 8. [Forms and Controlled State](#forms-and-controlled-state)
60
- 9. [Lazy Loading and Dynamic Data](#lazy-loading-and-dynamic-data)
61
- 10. [Dialogs and Overflow Containers](#dialogs-and-overflow-containers)
62
- 11. [Events](#events)
63
- 12. [Ref Methods](#ref-methods)
64
- 13. [Run Locally](#run-locally)
65
- 14. [License](#license)
68
+ 6. [Customization Paths](#customization-paths)
69
+ 7. [Slots API](#slots-api)
70
+ 8. [Headless Usage](#headless-usage)
71
+ 9. [Combobox Contract](#combobox-contract)
72
+ 10. [React 19 StackBlitz Playground](#react-19-stackblitz-playground)
73
+ 11. [Official React 19 Test Matrix](#official-react-19-test-matrix)
74
+ 12. [Custom Render Functions](#custom-render-functions)
75
+ 13. [Forms and Controlled State](#forms-and-controlled-state)
76
+ 14. [Lazy Loading and Dynamic Data](#lazy-loading-and-dynamic-data)
77
+ 15. [Dialogs and Overflow Containers](#dialogs-and-overflow-containers)
78
+ 16. [Events](#events)
79
+ 17. [Ref Methods](#ref-methods)
80
+ 18. [Run Locally](#run-locally)
81
+ 19. [License](#license)
66
82
 
67
83
  ## React Version Compatibility
68
84
 
@@ -70,17 +86,17 @@ Each package family installs on its matching React family. Keep the package fami
70
86
 
71
87
  | Package family | React family | Peer range | Tested release window | Demo link |
72
88
  | :---: | :---: | :---: | :---: | :--- |
73
- | **17.x** | **React 17 only** | **`>=17.0.0 <18.0.0`** | **17.0.0 -> 17.0.2** | [React 17 family docs](https://alexandro.net/docs/react/multiselect/react-17/) |
74
- | **18.x** | **React 18 only** | **`>=18.0.0 <19.0.0`** | Planned | [React docs](https://alexandro.net/docs/react/multiselect/) |
75
- | **19.x** | **React 19 only** | **`>=19.0.0 <20.0.0`** | Planned | [React docs](https://alexandro.net/docs/react/multiselect/) |
89
+ | **17.x** | **React 17 only** | **`>=17.0.0 <18.0.0`** | **17.0.2 -> 17.0.2** | [React 17 family docs](https://alexandro.net/docs/react/multiselect/react-17/) |
90
+ | **18.x** | **React 18 only** | **`>=18.0.0 <19.0.0`** | **18.0.2 -> 18.3.1** | [React 18 family docs](https://alexandro.net/docs/react/multiselect/react-18/) |
91
+ | **19.x** | **React 19 only** | **`>=19.0.0 <20.0.0`** | **19.1.3 -> 19.2.4** | [React 19 family docs](https://alexandro.net/docs/react/multiselect/react-19/) |
76
92
 
77
93
  ## Installation
78
94
 
79
95
  ```bash
80
- npm install @stackline/react-multiselect-dropdown@17.0.0 --save-exact
96
+ npm install @stackline/react-multiselect-dropdown@19.1.3 --save-exact
81
97
  ```
82
98
 
83
- Install `17.0.0` for React 17.x applications. The package includes its component styles and injects them at runtime, so no extra CSS import is required for the default experience.
99
+ Install `19.1.3` for React 19.x applications. The styled component includes its component styles and injects them at runtime. The headless hook does not inject CSS and lets your application own the markup and styling.
84
100
 
85
101
  ## Setup
86
102
 
@@ -142,7 +158,7 @@ Built-in skins:
142
158
  ## Basic Usage
143
159
 
144
160
  ```tsx
145
- import { useMemo, useState } from 'react';
161
+ import { useState } from 'react';
146
162
  import {
147
163
  MultiSelectDropdown,
148
164
  type DropdownSettings
@@ -204,9 +220,346 @@ export function CountrySelector() {
204
220
  }
205
221
  ```
206
222
 
207
- ## Official React 17 Test Matrix
223
+ ## Customization Paths
224
+
225
+ Use the API layer that matches the amount of control your team needs:
226
+
227
+ | Layer | Best for | What you own |
228
+ | :--- | :--- | :--- |
229
+ | `<MultiSelectDropdown />` | Fast forms, filters, dashboards, and admin screens. | Data, selected state, settings, events, and optional render functions. |
230
+ | `createMultiSelectDropdown<T>()` | Teams that want to bind the item type once for a feature or design-system wrapper. | A typed component family, typed settings, typed slots, and typed hooks. |
231
+ | `slots` | Custom HTML around the proven component behavior. | Specific DOM pieces such as trigger, badges, menu, search, groups, options, and footer. |
232
+ | `useMultiSelectDropdown` | Fully custom interfaces and design systems. | All markup and CSS, while Stackline provides state, ARIA prop getters, keyboard flow, grouping, and callbacks. |
233
+ | `useMultiSelectState` | Advanced state engines or existing combobox shells. | Every element, every event binding, and all visual behavior. |
234
+
235
+ For most teams, start with the component. Use `slots` when the component works but your layout needs a different shell. Use the headless hooks when the application must own the whole combobox contract.
236
+
237
+ ## Type-Safe Factory
238
+
239
+ Use `createMultiSelectDropdown<T>()` when a feature, package, or design-system wrapper should bind the item type once and reuse it across the component, settings, slots, and hooks.
240
+
241
+ This is optional. The normal `<MultiSelectDropdown />` API remains the fastest path for most screens.
242
+
243
+ ```tsx
244
+ import { useMemo, useState } from 'react';
245
+ import {
246
+ createMultiSelectDropdown
247
+ } from '@stackline/react-multiselect-dropdown';
248
+
249
+ type Country = {
250
+ id: number;
251
+ itemName: string;
252
+ capital: string;
253
+ region: string;
254
+ };
255
+
256
+ const CountryMultiselect = createMultiSelectDropdown<Country>();
257
+
258
+ const countrySettings = CountryMultiselect.defineSettings({
259
+ text: 'Choose countries',
260
+ primaryKey: 'id',
261
+ labelKey: 'itemName',
262
+ searchBy: ['itemName', 'capital'],
263
+ groupBy: 'region',
264
+ enableSearchFilter: true,
265
+ badgeShowLimit: 2
266
+ });
267
+
268
+ const countrySlots = CountryMultiselect.defineSlots({
269
+ Option: ({ props, option, checkbox }) => (
270
+ <div {...props}>
271
+ {checkbox}
272
+ <strong>{option.item.itemName}</strong>
273
+ <small>{option.item.capital} - {option.item.region}</small>
274
+ </div>
275
+ )
276
+ });
277
+
278
+ export function CountryFilter({ countries }: { countries: Country[] }) {
279
+ const [selectedCountries, setSelectedCountries] = useState<Country[]>([]);
280
+
281
+ return (
282
+ <CountryMultiselect.Dropdown
283
+ data={countries}
284
+ selectedItems={selectedCountries}
285
+ onChange={setSelectedCountries}
286
+ settings={countrySettings}
287
+ slots={countrySlots}
288
+ />
289
+ );
290
+ }
291
+ ```
292
+
293
+ With the factory, `primaryKey`, `labelKey`, `searchBy`, and `groupBy` are checked against `keyof Country`. Typos such as `labelKey: 'itemname'` or `searchBy: ['city']` fail at compile time.
294
+
295
+ ## Slots API
296
+
297
+ Slots let you replace the visible HTML pieces while the package still owns the tested selection, filtering, keyboard, focus, ARIA, body overlay, and async behavior.
298
+
299
+ The important rule is simple: spread the supplied `props` onto the element that plays that role. Those props carry the required refs, ARIA attributes, keyboard handlers, click handlers, ids, classes, and positioning styles.
300
+
301
+ ```tsx
302
+ import { useMemo, useState } from 'react';
303
+ import {
304
+ MultiSelectDropdown,
305
+ type DropdownSettings,
306
+ type MultiSelectDropdownSlots
307
+ } from '@stackline/react-multiselect-dropdown';
308
+
309
+ type Country = {
310
+ id: number;
311
+ itemName: string;
312
+ capital: string;
313
+ region: string;
314
+ };
315
+
316
+ const countrySlots: MultiSelectDropdownSlots<Country> = {
317
+ Trigger: ({ props, children, state }) => (
318
+ <div {...props} className={`${props.className} country-trigger`}>
319
+ <span className="country-trigger-summary">
320
+ {state.selectedItems.length ? `${state.selectedItems.length} selected` : 'Choose countries'}
321
+ </span>
322
+ {children}
323
+ </div>
324
+ ),
325
+ Badge: ({ props, item, removeButton }) => (
326
+ <span {...props} className={`${props.className} country-badge`}>
327
+ <strong>{item.itemName}</strong>
328
+ {removeButton}
329
+ </span>
330
+ ),
331
+ Option: ({ props, option, checkbox }) => (
332
+ <div {...props} className={`${props.className} country-option`}>
333
+ {checkbox}
334
+ <span>
335
+ <strong>{option.label}</strong>
336
+ <small>{option.item.capital} - {option.item.region}</small>
337
+ </span>
338
+ </div>
339
+ ),
340
+ MenuFooter: ({ props, state }) => (
341
+ <div {...props}>
342
+ {state.filteredItems.length} visible options
343
+ </div>
344
+ )
345
+ };
208
346
 
209
- The published React 17 release was tested in a clean React `17.0.2` application with `@stackline/react-multiselect-dropdown@17.0.0`. The docs use the same examples from that test app, including keyboard navigation, focus, ARIA behavior, badge counters, responsive action buttons, scrollable lists, and dialog-safe body overlays.
347
+ export function CountrySelector() {
348
+ const [selectedCountries, setSelectedCountries] = useState<Country[]>([]);
349
+ const settings = useMemo<DropdownSettings<Country>>(
350
+ () => ({
351
+ text: 'Choose countries',
352
+ enableSearchFilter: true,
353
+ groupBy: 'region',
354
+ primaryKey: 'id',
355
+ labelKey: 'itemName',
356
+ badgeShowLimit: 2,
357
+ skin: 'classic'
358
+ }),
359
+ []
360
+ );
361
+
362
+ return (
363
+ <MultiSelectDropdown
364
+ data={countries}
365
+ selectedItems={selectedCountries}
366
+ onChange={setSelectedCountries}
367
+ settings={settings}
368
+ slots={countrySlots}
369
+ />
370
+ );
371
+ }
372
+ ```
373
+
374
+ Available slots:
375
+
376
+ `Root`, `Trigger`, `Value`, `Placeholder`, `SingleValue`, `BadgeList`, `Badge`, `BadgeLabel`, `BadgeRemove`, `Actions`, `OverflowCounter`, `ClearAll`, `Arrow`, `Menu`, `Toolbar`, `BulkActions`, `SelectAll`, `AddNewItem`, `Search`, `OptionList`, `Group`, `GroupHeader`, `GroupAction`, `Option`, `Checkbox`, `LoadingState`, `EmptyState`, and `MenuFooter`.
377
+
378
+ ## Headless Usage
379
+
380
+ Use `useMultiSelectDropdown` when you want Stackline selection, filtering, keyboard handling, ARIA props, grouping, limits, and callbacks without the built-in DOM/CSS.
381
+
382
+ 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.
383
+
384
+ ```bash
385
+ npm install flag-icons
386
+ ```
387
+
388
+ ```tsx
389
+ import { useState } from 'react';
390
+ import 'flag-icons/css/flag-icons.min.css';
391
+ import { useMultiSelectDropdown } from '@stackline/react-multiselect-dropdown';
392
+
393
+ const countries = [
394
+ { id: 1, itemName: 'Brazil', flag: 'BR', capital: 'Brasilia', region: 'Americas' },
395
+ { id: 2, itemName: 'Canada', flag: 'CA', capital: 'Ottawa', region: 'Americas' },
396
+ { id: 3, itemName: 'Portugal', flag: 'PT', capital: 'Lisbon', region: 'Europe' }
397
+ ];
398
+
399
+ function flagClass(code: string) {
400
+ return `fi fi-${code.toLowerCase()}`;
401
+ }
402
+
403
+ export function HeadlessCountries() {
404
+ const [selectedItems, setSelectedItems] = useState([countries[0]]);
405
+ const dropdown = useMultiSelectDropdown({
406
+ data: countries,
407
+ selectedItems,
408
+ onChange: setSelectedItems,
409
+ settings: {
410
+ text: 'Choose countries',
411
+ enableSearchFilter: true,
412
+ searchPlaceholderText: 'Search country',
413
+ groupBy: 'region',
414
+ primaryKey: 'id',
415
+ labelKey: 'itemName',
416
+ badgeShowLimit: 2,
417
+ clearAll: true
418
+ }
419
+ });
420
+
421
+ return (
422
+ <div className="country-picker" {...dropdown.getRootProps()}>
423
+ <button className="country-trigger" {...dropdown.getTriggerProps()}>
424
+ <span>{dropdown.label}</span>
425
+ <strong>{dropdown.isOpen ? 'Close' : 'Open'}</strong>
426
+ </button>
427
+
428
+ <div className="country-chips">
429
+ {dropdown.selectedItems.map((item) => (
430
+ <span className="country-chip" key={dropdown.getItemKey(item)}>
431
+ <span className={`country-flag ${flagClass(item.flag)}`} aria-hidden="true" />
432
+ {dropdown.getItemLabel(item)}
433
+ <button {...dropdown.getRemoveButtonProps(item)}>x</button>
434
+ </span>
435
+ ))}
436
+ </div>
437
+
438
+ {dropdown.isOpen ? (
439
+ <div className="country-panel" {...dropdown.getListboxProps()}>
440
+ <input className="country-search" {...dropdown.getSearchInputProps()} />
441
+
442
+ {dropdown.visibleOptions.map((option) => (
443
+ <div
444
+ key={option.key}
445
+ {...dropdown.getOptionProps(option, {
446
+ className: option.selected ? 'country-option selected' : 'country-option'
447
+ })}>
448
+ <span className={`country-flag ${flagClass(option.item.flag)}`} aria-hidden="true" />
449
+ <span>
450
+ <strong>{option.label}</strong>
451
+ <small>{option.item.capital} - {option.item.region}</small>
452
+ </span>
453
+ <input type="checkbox" checked={option.selected} readOnly />
454
+ </div>
455
+ ))}
456
+ </div>
457
+ ) : null}
458
+ </div>
459
+ );
460
+ }
461
+ ```
462
+
463
+ Use `useMultiSelectState` when you want the selection/filter/grouping engine without prop getters:
464
+
465
+ ```tsx
466
+ import { useMultiSelectState } from '@stackline/react-multiselect-dropdown';
467
+
468
+ const state = useMultiSelectState({
469
+ data: countries,
470
+ selectedItems,
471
+ onChange: setSelectedItems,
472
+ settings: {
473
+ primaryKey: 'id',
474
+ labelKey: 'itemName',
475
+ enableSearchFilter: true
476
+ }
477
+ });
478
+ ```
479
+
480
+ The styled component remains available for drop-in usage. The headless hooks are for teams that want a headless-style ownership model where the application controls layout, elements, and CSS.
481
+
482
+ ## Combobox Contract
483
+
484
+ Version `19.1.3` tightens the interaction details that usually matter most in production forms:
485
+
486
+ | Behavior | Contract |
487
+ | :--- | :--- |
488
+ | Focus after selection/removal | Focus returns to search while the list stays open, or to the trigger when the list closes. |
489
+ | Option selection state | Multiselect options expose matching `aria-selected` and `aria-checked` values. |
490
+ | Backspace in search | Edits the query. With an empty query it does not remove selected values by default. |
491
+ | Backspace/Delete on focused badge remove button | Removes that selected badge. |
492
+ | Escape | Closes the list without clearing selected values. |
493
+ | Async add-item | Late async responses are ignored when a newer add request has already resolved. |
494
+ | Async option refresh | Selected object values are merged into the option source so values are not lost when data refreshes. |
495
+ | Keyboard navigation | Trigger ArrowDown/ArrowUp, option Home/End, and option ids keep focus and ARIA predictable. |
496
+
497
+ Keyboard behavior is enabled by default. You can turn each part off from `settings.keyboard` when an application needs a stricter interaction model:
498
+
499
+ ```tsx
500
+ const settings: DropdownSettings<Country> = {
501
+ text: 'Countries',
502
+ enableSearchFilter: true,
503
+ keyboard: {
504
+ space: true,
505
+ spaceOptionAction: 'toggle',
506
+ tab: true,
507
+ arrows: true,
508
+ escape: true,
509
+ backspaceRemovesLastWhenSearchEmpty: false,
510
+ deleteRemovesFocusedBadge: true
511
+ }
512
+ };
513
+ ```
514
+
515
+ 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:
516
+ `'toggle'` keeps focus on the current option, while `'toggle-and-next'` toggles and moves to the next enabled option.
517
+ `escapeToClose: false` is still supported and also disables `keyboard.escape`.
518
+
519
+ ## React 19 StackBlitz Playground
520
+
521
+ Use the dedicated React 19 StackBlitz project when you want a fast editable example without importing the full package repository:
522
+
523
+ **[Open the React 19 playground](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fbasic%2Fbasic.component.tsx&startScript=start&initialpath=%2Fbasic)**
524
+
525
+ | Example | StackBlitz |
526
+ | :--- | :--- |
527
+ | Basic usage | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fbasic%2Fbasic.component.tsx&startScript=start&initialpath=%2Fbasic) |
528
+ | Keyboard contract | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fkeyboard-contract%2Fkeyboard-contract.component.tsx&startScript=start&initialpath=%2Fkeyboard-contract) |
529
+ | ARIA state audit | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Faria-state%2Faria-state.component.tsx&startScript=start&initialpath=%2Faria-state) |
530
+ | Headless + ARIA | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fheadless-aria%2Fheadless-aria.component.tsx&startScript=start&initialpath=%2Fheadless-aria) |
531
+ | State hook | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fstate-hook%2Fstate-hook.component.tsx&startScript=start&initialpath=%2Fstate-hook) |
532
+ | Slots API | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fslots-api%2Fslots-api.component.tsx&startScript=start&initialpath=%2Fslots-api) |
533
+ | Type-safe factory | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Ftype-safe-factory%2Ftype-safe-factory.component.tsx&startScript=start&initialpath=%2Ftype-safe-factory) |
534
+ | Async object preservation | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fasync-object-preservation%2Fasync-object-preservation.component.tsx&startScript=start&initialpath=%2Fasync-object-preservation) |
535
+ | Single selection | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fsingle-selection%2Fsingle-selection.component.tsx&startScript=start&initialpath=%2Fsingle-selection) |
536
+ | Search filter | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fsearch-filter%2Fsearch-filter.component.tsx&startScript=start&initialpath=%2Fsearch-filter) |
537
+ | Custom search from API | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fcustom-search-api%2Fcustom-search-api.component.tsx&startScript=start&initialpath=%2Fcustom-search-api) |
538
+ | Search filter by property | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fsearch-filter-by-property%2Fsearch-filter-by-property.component.tsx&startScript=start&initialpath=%2Fsearch-filter-by-property) |
539
+ | Search and Add New Item | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fsearch-add-new-item%2Fsearch-add-new-item.component.tsx&startScript=start&initialpath=%2Fsearch-add-new-item) |
540
+ | Group By | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fgroup-by%2Fgroup-by.component.tsx&startScript=start&initialpath=%2Fgroup-by) |
541
+ | Templating | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Ftemplating%2Ftemplating.component.tsx&startScript=start&initialpath=%2Ftemplating) |
542
+ | Template-style forms | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Ftemplate-driven-forms%2Ftemplate-driven-forms.component.tsx&startScript=start&initialpath=%2Ftemplate-driven-forms) |
543
+ | Reactive forms | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Freactive-forms%2Freactive-forms.component.tsx&startScript=start&initialpath=%2Freactive-forms) |
544
+ | Long List Keyboard Scroll | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fvirtual-scrolling%2Fvirtual-scrolling.component.tsx&startScript=start&initialpath=%2Fvirtual-scrolling) |
545
+ | Lazy Loading from API | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Flazy-loading-api%2Flazy-loading-api.component.tsx&startScript=start&initialpath=%2Flazy-loading-api) |
546
+ | Data from remote API | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fremote-data%2Fremote-data.component.tsx&startScript=start&initialpath=%2Fremote-data) |
547
+ | Using in list for loop | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Flist-loop%2Flist-loop.component.tsx&startScript=start&initialpath=%2Flist-loop) |
548
+ | Using inside dialog | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fdialog%2Fdialog.component.tsx&startScript=start&initialpath=%2Fdialog) |
549
+ | Multiple dropdowns | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fmultiple-dropdowns%2Fmultiple-dropdowns.component.tsx&startScript=start&initialpath=%2Fmultiple-dropdowns) |
550
+ | Load dynamic data | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fdynamic-data%2Fdynamic-data.component.tsx&startScript=start&initialpath=%2Fdynamic-data) |
551
+ | Methods | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fmethods%2Fmethods.component.tsx&startScript=start&initialpath=%2Fmethods) |
552
+ | Events | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fevents%2Fevents.component.tsx&startScript=start&initialpath=%2Fevents) |
553
+ | Disabled state | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fdisabled%2Fdisabled.component.tsx&startScript=start&initialpath=%2Fdisabled) |
554
+ | Limit selection | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Flimit-selection%2Flimit-selection.component.tsx&startScript=start&initialpath=%2Flimit-selection) |
555
+ | Limit badges | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Flimit-badges%2Flimit-badges.component.tsx&startScript=start&initialpath=%2Flimit-badges) |
556
+ | Custom placeholder | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fcustom-placeholder%2Fcustom-placeholder.component.tsx&startScript=start&initialpath=%2Fcustom-placeholder) |
557
+ | Styling | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fstyling%2Fstyling.component.tsx&startScript=start&initialpath=%2Fstyling) |
558
+ | Body Overlay Auto | [Open](https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fbody-overlay-auto%2Fbody-overlay-auto.component.tsx&startScript=start&initialpath=%2Fbody-overlay-auto) |
559
+
560
+ ## Official React 19 Test Matrix
561
+
562
+ The React 19 release was tested in a clean React `19.2.4` application with `@stackline/react-multiselect-dropdown@19.1.3`. 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, guided Slots API customization, headless/custom HTML, and the combobox contract checks for Backspace, Escape, focused badge removal, focus, and option ARIA.
210
563
 
211
564
  The same core scenarios are validated for the visual skins:
212
565
 
@@ -227,10 +580,11 @@ The same core scenarios are validated for the visual skins:
227
580
  | 13 | Dialog and overflow container | `{ appendToBody: true, tagToBody: true }` |
228
581
  | 14 | Body overlay auto direction | `{ autoPosition: true, position: 'top' }` |
229
582
  | 15 | Ref methods | `openDropdown`, `closeDropdown`, `selectAll`, `clearSelection` |
583
+ | 16 | Slots API custom HTML | `slots.Trigger`, `slots.Option`, `slots.Search`, `slots.GroupHeader`, `slots.MenuFooter` |
230
584
 
231
585
  ## Custom Render Functions
232
586
 
233
- Use `renderItem` for option rows and `renderBadge` for selected chips:
587
+ Use `renderItem` for option rows and `renderBadge` for selected chips when you only need to replace inner content. Use `slots` when you need to replace component structure such as the trigger, menu, search shell, option row, badge, or footer.
234
588
 
235
589
  ```tsx
236
590
  <MultiSelectDropdown
@@ -361,10 +715,10 @@ npm run build
361
715
  npm test
362
716
  ```
363
717
 
364
- React 17 docs:
718
+ React 19 docs:
365
719
 
366
720
  ```bash
367
- cd docs-src/react-17
721
+ cd docs-src/react-19
368
722
  npm install
369
723
  npm run build
370
724
  ```