@xsolla/xui-multi-select 0.216.0 → 0.217.0

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
@@ -50,7 +50,7 @@ A dropdown input that allows selecting multiple values simultaneously. Displays
50
50
  - When State=Error, the error message must be linked via aria-describedby and in an aria-live=*"polite"* region.
51
51
  - The ContextMenu dropdown uses role=*"listbox"* (or role=*"menu"*) with each option as role=*"option"* / role=*"menuitemcheckbox"* and aria-checked=*"true"* / *"false"*.
52
52
  - Keyboard navigation: Enter / Space / ↓ opens the dropdown; ↑ / ↓ navigate options; Space toggles the focused option; Escape closes without further changes; Tab closes and moves focus forward.
53
- - When a value is added or removed (via chip ✕ or from dropdown), announce the change via aria-live=*"polite"*: *"Design added"* or *"Design removed"*.
53
+ - When a value is added or removed (via chip ✕ or from dropdown), announce the change via aria-live=*"polite"*: *"Design added"* or *"Design removed"*.
54
54
  <!-- END:xui-mcp-instructions:multi-select -->
55
55
 
56
56
  ## Installation
@@ -158,12 +158,15 @@ Add backdrop, click-outside, and Escape handling in your layout as needed (see S
158
158
  | `errorMessage` | `string` | — | Error message; also marks the control invalid. |
159
159
  | `variant` | `'tag' \| 'text'` | `'tag'` | How selected options are displayed. |
160
160
  | `flexible` | `boolean` | `true` | When `true` the control grows with content; otherwise fixed-height. |
161
- | `removeTagsButtons` | `boolean` | `true` | Show a remove button on each tag. |
162
- | `extraClear` | `boolean` | `false` | Show a clear-all button. |
161
+ | `removeTagsButtons` | `boolean` | `true` | Show a remove button on each tag. Component-wide default — override per option with `MultiSelectOption.removable`. |
162
+ | `extraClear` | `boolean` | `false` | Show a clear-all button. Options with `removable: false` are kept. |
163
163
  | `maxHeight` | `number` | `300` | Maximum dropdown height in pixels. |
164
164
  | `searchable` | `boolean` | `false` | Show a search input at the top of the built-in dropdown that filters options in memory by label. Web only. |
165
165
  | `searchPlaceholder` | `string` | `'Search'` | Placeholder for the search input (used when `searchable`). |
166
166
  | `noOptionsMessage` | `string` | `'No results'` | Message shown when the search filter matches no options. |
167
+ | `selectAll` | `boolean` | `false` | Show a select-all / deselect-all row at the top of the built-in dropdown. Scoped to the options currently listed; skips `disabled`; never deselects locked options. Ignored when `dropdownMenu` is `false`. |
168
+ | `selectAllLabel` | `string` | `'Select all'` | Label of the select-all row (used when `selectAll`). |
169
+ | `deselectAllLabel` | `string` | `'Deselect all'` | Label of the row once everything listed is selected (used when `selectAll`). |
167
170
  | `iconLeft` | `ReactNode` | — | Icon rendered on the left of the control. |
168
171
  | `iconRight` | `ReactNode` | — | Icon on the right (overrides the default caret). |
169
172
  | `dropdownMenu` | `boolean` | `true` | When `false`, hides the built-in list and disables click-to-open; use with an external picker (e.g. `GroupSelect`) wired to the same `value` / `onChange`. |
@@ -184,10 +187,28 @@ type MultiSelectState = "default" | "hover" | "focus" | "disable" | "error";
184
187
  interface MultiSelectOption {
185
188
  label: ReactNode;
186
189
  value: string | number;
190
+ /** Greys the option out and blocks interaction. */
187
191
  disabled?: boolean;
192
+ /** When `false`, the option is locked: selected and not removable. Defaults to `true`. */
193
+ removable?: boolean;
188
194
  }
189
195
  ```
190
196
 
197
+ #### `disabled` vs `removable`
198
+
199
+ They solve different problems and can be combined:
200
+
201
+ | | `disabled: true` | `removable: false` |
202
+ | --- | --- | --- |
203
+ | Greyed out | Yes | No — renders like any other selection |
204
+ | Can be selected by the user | No | Yes |
205
+ | Can be deselected once selected | Yes (the flag does not lock an existing selection) | No |
206
+ | Tag shows the ✕ remove button | Yes | No |
207
+ | Included by select-all / deselect-all | No (skipped) | Selected yes, deselected no |
208
+ | Removed by `extraClear` | Yes | No |
209
+
210
+ Use `removable: false` for a required, default-selected value that should still look like a normal selection.
211
+
191
212
  ## Examples
192
213
 
193
214
  ### Sizes
@@ -285,6 +306,62 @@ const [selected, setSelected] = useState<MultiSelectValue>([]);
285
306
  />;
286
307
  ```
287
308
 
309
+ ### Select all
310
+
311
+ Enable `selectAll` to show a select-all row at the top of the dropdown, so a long list does not have to be clicked through option by option. Once every selectable listed option is selected, the row flips to deselect-all. Customize the two labels with `selectAllLabel` / `deselectAllLabel`.
312
+
313
+ Scope rules:
314
+
315
+ - **Filtered, not global.** With `searchable` active and a query typed, select-all applies only to the options currently listed — it never silently selects options the user cannot see. Deselect-all likewise only clears the listed ones.
316
+ - **`disabled` options are skipped** in both directions.
317
+ - **`removable: false` options are never deselected** — deselect-all leaves them in place.
318
+
319
+ ```tsx
320
+ const options = [
321
+ { label: "React", value: "react" },
322
+ { label: "Vue", value: "vue" },
323
+ { label: "Angular", value: "angular" },
324
+ { label: "Svelte", value: "svelte" },
325
+ ];
326
+
327
+ const [selected, setSelected] = useState<MultiSelectValue>([]);
328
+
329
+ <MultiSelect
330
+ label="Frameworks"
331
+ options={options}
332
+ value={selected}
333
+ onChange={setSelected}
334
+ selectAll
335
+ searchable
336
+ placeholder="Select frameworks"
337
+ />;
338
+ ```
339
+
340
+ ### Locked (non-removable) options
341
+
342
+ `removeTagsButtons` is component-wide: it either shows the ✕ on every tag or on none. To lock a single value, set `removable: false` on that option instead. Its tag renders without a ✕, clicking the tag does nothing, unchecking it in the dropdown is a no-op, and `extraClear` / deselect-all leave it selected. Unlike `disabled`, it is not greyed out — so a required default reads as a normal selection.
343
+
344
+ ```tsx
345
+ // English is required and must always stay selected; the rest are optional.
346
+ const languages = [
347
+ { label: "English", value: "en", removable: false },
348
+ { label: "German", value: "de" },
349
+ { label: "French", value: "fr" },
350
+ { label: "Japanese", value: "ja" },
351
+ ];
352
+
353
+ const [selected, setSelected] = useState<MultiSelectValue>(["en"]);
354
+
355
+ <MultiSelect
356
+ label="Languages"
357
+ options={languages}
358
+ value={selected}
359
+ onChange={setSelected}
360
+ extraClear
361
+ placeholder="Select languages"
362
+ />;
363
+ ```
364
+
288
365
  ### Long option labels
289
366
 
290
367
  Long, unbreakable option labels (URLs, IDs, tokens) wrap inside the dropdown menu instead of forcing a horizontal scrollbar; the menu width stays pinned to the control.
@@ -316,3 +393,5 @@ const [selected, setSelected] = useState<MultiSelectValue>([]);
316
393
  - The dropdown is keyboard navigable; selection state is announced to assistive technology.
317
394
  - An `errorMessage` marks the control as invalid for screen readers.
318
395
  - When `searchable`, the dropdown's search input exposes an `aria-label` matching `searchPlaceholder`; options filter in memory as the user types (web only).
396
+ - When `selectAll`, the select-all row is exposed as a button whose `aria-label` matches its current action (`selectAllLabel` / `deselectAllLabel`), so the select ↔ deselect flip is announced.
397
+ - A `removable: false` option renders no remove button, so assistive technology does not offer a removal action that would fail.
@@ -11,6 +11,19 @@ interface MultiSelectOption {
11
11
  label: ReactNode;
12
12
  value: string | number;
13
13
  disabled?: boolean;
14
+ /**
15
+ * When `false`, this option is **locked**: once selected it cannot be
16
+ * removed. Its tag renders without a remove button, clicking the tag does
17
+ * nothing, toggling the option off in the dropdown is a no-op, the
18
+ * `extraClear` clear-all button leaves it in place, and select-all /
19
+ * deselect-all skips it.
20
+ *
21
+ * Unlike `disabled`, a locked option is **not** greyed out — use it for a
22
+ * required, default-selected value (e.g. a base language that must always
23
+ * stay selected) that should still look like a normal selection.
24
+ * @default true
25
+ */
26
+ removable?: boolean;
14
27
  }
15
28
  interface MultiSelectProps extends ThemeOverrideProps {
16
29
  /**
@@ -70,12 +83,14 @@ interface MultiSelectProps extends ThemeOverrideProps {
70
83
  */
71
84
  flexible?: boolean;
72
85
  /**
73
- * Switch on displaying a remove button in each tag.
86
+ * Switch on displaying a remove button in each tag. This is the
87
+ * component-wide default; an individual option can opt out with
88
+ * `MultiSelectOption.removable: false`.
74
89
  * @default true
75
90
  */
76
91
  removeTagsButtons?: boolean;
77
92
  /**
78
- * Add clear all button.
93
+ * Add clear all button. Options with `removable: false` are kept.
79
94
  * @default false
80
95
  */
81
96
  extraClear?: boolean;
@@ -113,6 +128,32 @@ interface MultiSelectProps extends ThemeOverrideProps {
113
128
  * @default "No results"
114
129
  */
115
130
  noOptionsMessage?: string;
131
+ /**
132
+ * When true, shows a select-all / deselect-all row at the top of the
133
+ * built-in dropdown (below the search input when `searchable`).
134
+ *
135
+ * Scope is **the options currently listed**: with `searchable` active and a
136
+ * search query typed, it applies only to the filtered options, matching what
137
+ * the user can see. Options with `disabled: true` are skipped, and options
138
+ * with `removable: false` are never deselected. The row flips to
139
+ * deselect-all once every selectable listed option is selected.
140
+ *
141
+ * Ignored when `dropdownMenu` is false.
142
+ * @default false
143
+ */
144
+ selectAll?: boolean;
145
+ /**
146
+ * Label of the select-all row. Only used when `selectAll` is true.
147
+ * @default "Select all"
148
+ */
149
+ selectAllLabel?: string;
150
+ /**
151
+ * Label of the select-all row once everything listed is selected, at which
152
+ * point activating it clears the listed selection. Only used when
153
+ * `selectAll` is true.
154
+ * @default "Deselect all"
155
+ */
156
+ deselectAllLabel?: string;
116
157
  /**
117
158
  * When false, the built-in options list and backdrop are not shown and the control
118
159
  * does not open on click. Use with an external picker (e.g. grouped select) wired
package/native/index.d.ts CHANGED
@@ -11,6 +11,19 @@ interface MultiSelectOption {
11
11
  label: ReactNode;
12
12
  value: string | number;
13
13
  disabled?: boolean;
14
+ /**
15
+ * When `false`, this option is **locked**: once selected it cannot be
16
+ * removed. Its tag renders without a remove button, clicking the tag does
17
+ * nothing, toggling the option off in the dropdown is a no-op, the
18
+ * `extraClear` clear-all button leaves it in place, and select-all /
19
+ * deselect-all skips it.
20
+ *
21
+ * Unlike `disabled`, a locked option is **not** greyed out — use it for a
22
+ * required, default-selected value (e.g. a base language that must always
23
+ * stay selected) that should still look like a normal selection.
24
+ * @default true
25
+ */
26
+ removable?: boolean;
14
27
  }
15
28
  interface MultiSelectProps extends ThemeOverrideProps {
16
29
  /**
@@ -70,12 +83,14 @@ interface MultiSelectProps extends ThemeOverrideProps {
70
83
  */
71
84
  flexible?: boolean;
72
85
  /**
73
- * Switch on displaying a remove button in each tag.
86
+ * Switch on displaying a remove button in each tag. This is the
87
+ * component-wide default; an individual option can opt out with
88
+ * `MultiSelectOption.removable: false`.
74
89
  * @default true
75
90
  */
76
91
  removeTagsButtons?: boolean;
77
92
  /**
78
- * Add clear all button.
93
+ * Add clear all button. Options with `removable: false` are kept.
79
94
  * @default false
80
95
  */
81
96
  extraClear?: boolean;
@@ -113,6 +128,32 @@ interface MultiSelectProps extends ThemeOverrideProps {
113
128
  * @default "No results"
114
129
  */
115
130
  noOptionsMessage?: string;
131
+ /**
132
+ * When true, shows a select-all / deselect-all row at the top of the
133
+ * built-in dropdown (below the search input when `searchable`).
134
+ *
135
+ * Scope is **the options currently listed**: with `searchable` active and a
136
+ * search query typed, it applies only to the filtered options, matching what
137
+ * the user can see. Options with `disabled: true` are skipped, and options
138
+ * with `removable: false` are never deselected. The row flips to
139
+ * deselect-all once every selectable listed option is selected.
140
+ *
141
+ * Ignored when `dropdownMenu` is false.
142
+ * @default false
143
+ */
144
+ selectAll?: boolean;
145
+ /**
146
+ * Label of the select-all row. Only used when `selectAll` is true.
147
+ * @default "Select all"
148
+ */
149
+ selectAllLabel?: string;
150
+ /**
151
+ * Label of the select-all row once everything listed is selected, at which
152
+ * point activating it clears the listed selection. Only used when
153
+ * `selectAll` is true.
154
+ * @default "Deselect all"
155
+ */
156
+ deselectAllLabel?: string;
116
157
  /**
117
158
  * When false, the built-in options list and backdrop are not shown and the control
118
159
  * does not open on click. Use with an external picker (e.g. grouped select) wired