@stackline/react-multiselect-dropdown 19.1.0 → 19.1.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
@@ -3,16 +3,19 @@
3
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
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 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)**
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 React 19 release:** `19.1.0` for React `19.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.2` for React `19.x`
16
19
 
17
20
  ---
18
21
 
@@ -26,7 +29,7 @@
26
29
 
27
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 stable React 19 release is `19.1.0`. 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.
32
+ The current stable React 19 release is `19.1.2`. 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
 
@@ -83,17 +86,17 @@ Each package family installs on its matching React family. Keep the package fami
83
86
 
84
87
  | Package family | React family | Peer range | Tested release window | Demo link |
85
88
  | :---: | :---: | :---: | :---: | :--- |
86
- | **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/) |
87
- | **18.x** | **React 18 only** | **`>=18.0.0 <19.0.0`** | **18.0.0 -> 18.3.1** | [React 18 family docs](https://alexandro.net/docs/react/multiselect/react-18/) |
88
- | **19.x** | **React 19 only** | **`>=19.0.0 <20.0.0`** | **19.1.0 -> 19.2.4** | [React 19 family docs](https://alexandro.net/docs/react/multiselect/react-19/) |
89
+ | **17.x** | **React 17 only** | **`>=17.0.0 <18.0.0`** | **17.0.1 -> 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.1 -> 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.2 -> 19.2.4** | [React 19 family docs](https://alexandro.net/docs/react/multiselect/react-19/) |
89
92
 
90
93
  ## Installation
91
94
 
92
95
  ```bash
93
- npm install @stackline/react-multiselect-dropdown@19.1.0 --save-exact
96
+ npm install @stackline/react-multiselect-dropdown@19.1.1 --save-exact
94
97
  ```
95
98
 
96
- Install `19.1.0` 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.
99
+ Install `19.1.2` 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.
97
100
 
98
101
  ## Setup
99
102
 
@@ -478,7 +481,7 @@ The styled component remains available for drop-in usage. The headless hooks are
478
481
 
479
482
  ## Combobox Contract
480
483
 
481
- Version `19.1.0` tightens the interaction details that usually matter most in production forms:
484
+ Version `19.1.2` tightens the interaction details that usually matter most in production forms:
482
485
 
483
486
  | Behavior | Contract |
484
487
  | :--- | :--- |
@@ -522,6 +525,13 @@ Use the dedicated React 19 StackBlitz project when you want a fast editable exam
522
525
  | Example | StackBlitz |
523
526
  | :--- | :--- |
524
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) |
525
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) |
526
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) |
527
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) |
@@ -549,7 +559,7 @@ Use the dedicated React 19 StackBlitz project when you want a fast editable exam
549
559
 
550
560
  ## Official React 19 Test Matrix
551
561
 
552
- The React 19 release was tested in a clean React `19.2.4` application with `@stackline/react-multiselect-dropdown@19.1.0`. 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.
562
+ The React 19 release was tested in a clean React `19.2.4` application with `@stackline/react-multiselect-dropdown@19.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, guided Slots API customization, headless/custom HTML, and the combobox contract checks for Backspace, Escape, focused badge removal, focus, and option ARIA.
553
563
 
554
564
  The same core scenarios are validated for the visual skins:
555
565
 
package/dist/index.cjs CHANGED
@@ -1518,6 +1518,11 @@ function InnerMultiSelectDropdown({
1518
1518
  return;
1519
1519
  }
1520
1520
  if (isSelected(item)) {
1521
+ if (settings.singleSelection) {
1522
+ closeDropdown(true);
1523
+ focusAfterSelectionChange("trigger");
1524
+ return;
1525
+ }
1521
1526
  removeItem(item, focusTarget);
1522
1527
  return;
1523
1528
  }
@@ -1928,7 +1933,7 @@ function InnerMultiSelectDropdown({
1928
1933
  }
1929
1934
  if (event.key === "Enter" || isSpaceKey(event.key)) {
1930
1935
  event.preventDefault();
1931
- const willClose = !isSelected(item) && (settings.singleSelection || settings.closeDropDownOnSelection);
1936
+ const willClose = settings.singleSelection || !isSelected(item) && settings.closeDropDownOnSelection;
1932
1937
  const currentOptionId = event.currentTarget.id;
1933
1938
  const moveToNextOption = isSpaceKey(event.key) && settings.keyboard.spaceOptionAction === "toggle-and-next";
1934
1939
  selectItem(item, willClose ? "trigger" : "none");
@@ -2120,7 +2125,7 @@ function InnerMultiSelectDropdown({
2120
2125
  if (disabled) {
2121
2126
  return;
2122
2127
  }
2123
- const willClose = !isSelected(item) && (settings.singleSelection || settings.closeDropDownOnSelection);
2128
+ const willClose = settings.singleSelection || !isSelected(item) && settings.closeDropDownOnSelection;
2124
2129
  selectItem(item, willClose ? "trigger" : "none");
2125
2130
  if (!willClose) {
2126
2131
  focusOptionAfterPointerSelection(optionId, optionIndex);
@@ -2610,6 +2615,10 @@ function useMultiSelectState({
2610
2615
  return;
2611
2616
  }
2612
2617
  if (isSelected(item)) {
2618
+ if (settings.singleSelection) {
2619
+ onSelectionShouldClose?.();
2620
+ return;
2621
+ }
2613
2622
  removeItem(item);
2614
2623
  return;
2615
2624
  }
@@ -2934,7 +2943,7 @@ function useMultiSelectDropdown({
2934
2943
  };
2935
2944
  const selectItem = (item, focusTarget = "search") => {
2936
2945
  const wasSelected = state.isSelected(item);
2937
- const willClose = !wasSelected && (settings.singleSelection || settings.closeDropDownOnSelection);
2946
+ const willClose = settings.singleSelection || !wasSelected && settings.closeDropDownOnSelection;
2938
2947
  state.selectItem(item);
2939
2948
  focusAfterSelectionChange(willClose ? "trigger" : focusTarget);
2940
2949
  };
@@ -3049,7 +3058,7 @@ function useMultiSelectDropdown({
3049
3058
  event.preventDefault();
3050
3059
  const enabledOptions2 = options.filter((currentOption) => !currentOption.disabled);
3051
3060
  const currentEnabledIndex2 = enabledOptions2.findIndex((currentOption) => currentOption.id === option.id);
3052
- const willClose = !state.isSelected(option.item) && (settings.singleSelection || settings.closeDropDownOnSelection);
3061
+ const willClose = settings.singleSelection || !state.isSelected(option.item) && settings.closeDropDownOnSelection;
3053
3062
  const moveToNextOption = isSpaceKey2(event.key) && settings.keyboard.spaceOptionAction === "toggle-and-next";
3054
3063
  selectItem(option.item, willClose ? "trigger" : "none");
3055
3064
  if (!willClose) {
@@ -3286,7 +3295,7 @@ function useMultiSelectDropdown({
3286
3295
  if (option.disabled) {
3287
3296
  return;
3288
3297
  }
3289
- const willClose = !state.isSelected(option.item) && (settings.singleSelection || settings.closeDropDownOnSelection);
3298
+ const willClose = settings.singleSelection || !state.isSelected(option.item) && settings.closeDropDownOnSelection;
3290
3299
  selectItem(option.item, willClose ? "trigger" : "none");
3291
3300
  if (!willClose) {
3292
3301
  setActiveOptionIndex(option.index);
package/dist/index.js CHANGED
@@ -1497,6 +1497,11 @@ function InnerMultiSelectDropdown({
1497
1497
  return;
1498
1498
  }
1499
1499
  if (isSelected(item)) {
1500
+ if (settings.singleSelection) {
1501
+ closeDropdown(true);
1502
+ focusAfterSelectionChange("trigger");
1503
+ return;
1504
+ }
1500
1505
  removeItem(item, focusTarget);
1501
1506
  return;
1502
1507
  }
@@ -1907,7 +1912,7 @@ function InnerMultiSelectDropdown({
1907
1912
  }
1908
1913
  if (event.key === "Enter" || isSpaceKey(event.key)) {
1909
1914
  event.preventDefault();
1910
- const willClose = !isSelected(item) && (settings.singleSelection || settings.closeDropDownOnSelection);
1915
+ const willClose = settings.singleSelection || !isSelected(item) && settings.closeDropDownOnSelection;
1911
1916
  const currentOptionId = event.currentTarget.id;
1912
1917
  const moveToNextOption = isSpaceKey(event.key) && settings.keyboard.spaceOptionAction === "toggle-and-next";
1913
1918
  selectItem(item, willClose ? "trigger" : "none");
@@ -2099,7 +2104,7 @@ function InnerMultiSelectDropdown({
2099
2104
  if (disabled) {
2100
2105
  return;
2101
2106
  }
2102
- const willClose = !isSelected(item) && (settings.singleSelection || settings.closeDropDownOnSelection);
2107
+ const willClose = settings.singleSelection || !isSelected(item) && settings.closeDropDownOnSelection;
2103
2108
  selectItem(item, willClose ? "trigger" : "none");
2104
2109
  if (!willClose) {
2105
2110
  focusOptionAfterPointerSelection(optionId, optionIndex);
@@ -2595,6 +2600,10 @@ function useMultiSelectState({
2595
2600
  return;
2596
2601
  }
2597
2602
  if (isSelected(item)) {
2603
+ if (settings.singleSelection) {
2604
+ onSelectionShouldClose?.();
2605
+ return;
2606
+ }
2598
2607
  removeItem(item);
2599
2608
  return;
2600
2609
  }
@@ -2919,7 +2928,7 @@ function useMultiSelectDropdown({
2919
2928
  };
2920
2929
  const selectItem = (item, focusTarget = "search") => {
2921
2930
  const wasSelected = state.isSelected(item);
2922
- const willClose = !wasSelected && (settings.singleSelection || settings.closeDropDownOnSelection);
2931
+ const willClose = settings.singleSelection || !wasSelected && settings.closeDropDownOnSelection;
2923
2932
  state.selectItem(item);
2924
2933
  focusAfterSelectionChange(willClose ? "trigger" : focusTarget);
2925
2934
  };
@@ -3034,7 +3043,7 @@ function useMultiSelectDropdown({
3034
3043
  event.preventDefault();
3035
3044
  const enabledOptions2 = options.filter((currentOption) => !currentOption.disabled);
3036
3045
  const currentEnabledIndex2 = enabledOptions2.findIndex((currentOption) => currentOption.id === option.id);
3037
- const willClose = !state.isSelected(option.item) && (settings.singleSelection || settings.closeDropDownOnSelection);
3046
+ const willClose = settings.singleSelection || !state.isSelected(option.item) && settings.closeDropDownOnSelection;
3038
3047
  const moveToNextOption = isSpaceKey2(event.key) && settings.keyboard.spaceOptionAction === "toggle-and-next";
3039
3048
  selectItem(option.item, willClose ? "trigger" : "none");
3040
3049
  if (!willClose) {
@@ -3271,7 +3280,7 @@ function useMultiSelectDropdown({
3271
3280
  if (option.disabled) {
3272
3281
  return;
3273
3282
  }
3274
- const willClose = !state.isSelected(option.item) && (settings.singleSelection || settings.closeDropDownOnSelection);
3283
+ const willClose = settings.singleSelection || !state.isSelected(option.item) && settings.closeDropDownOnSelection;
3275
3284
  selectItem(option.item, willClose ? "trigger" : "none");
3276
3285
  if (!willClose) {
3277
3286
  setActiveOptionIndex(option.index);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackline/react-multiselect-dropdown",
3
- "version": "19.1.0",
3
+ "version": "19.1.2",
4
4
  "description": "Maintained React 19 multiselect dropdown with accessibility-focused and keyboard/ARIA tested support, controlled state, slots, headless/state hooks, Stackline skins, body overlays, live docs, search, grouping, lazy loading, and custom renderers.",
5
5
  "keywords": [
6
6
  "react",