@xsolla/xui-multi-select 0.216.1 → 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
package/native/index.js CHANGED
@@ -435,7 +435,8 @@ var useMultiSelectControl = ({
435
435
  stateList: stateList.map((item) => item.value),
436
436
  variant,
437
437
  size,
438
- removeTagsButtons
438
+ removeTagsButtons,
439
+ lockedList: stateList.filter((item) => item.removable === false).map((item) => item.value)
439
440
  }),
440
441
  [stateList, variant, size, removeTagsButtons]
441
442
  );
@@ -448,11 +449,18 @@ var useMultiSelectControl = ({
448
449
  return true;
449
450
  if (prev.stateList.length !== widthsDependencies.stateList.length)
450
451
  return true;
452
+ if (prev.lockedList.length !== widthsDependencies.lockedList.length)
453
+ return true;
451
454
  let i = 0;
452
455
  for (const v of prev.stateList) {
453
456
  if (v !== widthsDependencies.stateList[i]) return true;
454
457
  i++;
455
458
  }
459
+ let j = 0;
460
+ for (const v of prev.lockedList) {
461
+ if (v !== widthsDependencies.lockedList[j]) return true;
462
+ j++;
463
+ }
456
464
  return false;
457
465
  };
458
466
  const setPlaceholder = () => {
@@ -506,26 +514,28 @@ var useMultiSelectControl = ({
506
514
  }
507
515
  const content = displayedItems.map((item, index) => {
508
516
  const isCount = item.value === COUNT_ITEM_VALUE;
517
+ const isLocked = !isCount && item.removable === false;
518
+ const canRemove = !isCount && !isLocked;
509
519
  const isReducable = index === 0 && displayStateRef.current === 3 /* Render */ && displayedItemsCount === 0 || flexible;
510
520
  if (variant === "tag") {
511
521
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
512
522
  Box,
513
523
  {
514
- onPress: !isCount ? (e) => {
524
+ onPress: canRemove ? (e) => {
515
525
  e.stopPropagation();
516
526
  if (!disabled) removeValue(item.value, e);
517
527
  } : void 0,
518
528
  style: {
519
529
  minWidth: isReducable ? 0 : "fit-content",
520
530
  opacity: disabled ? 0.5 : 1,
521
- cursor: disabled || isCount ? "not-allowed" : "pointer"
531
+ cursor: isLocked ? "default" : disabled || isCount ? "not-allowed" : "pointer"
522
532
  },
523
533
  children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
524
534
  import_xui_tag.Tag,
525
535
  {
526
536
  size,
527
537
  tone: "primary",
528
- onRemove: removeTagsButtons && !isCount ? (e) => {
538
+ onRemove: removeTagsButtons && canRemove ? (e) => {
529
539
  e?.stopPropagation();
530
540
  if (!disabled) removeValue(item.value, e);
531
541
  } : void 0,
@@ -784,6 +794,7 @@ var initialState = {
784
794
  selectedItems: [],
785
795
  isOpen: false
786
796
  };
797
+ var isLockedOption = (option) => option.removable === false;
787
798
  var useMultiSelect = ({
788
799
  options,
789
800
  value = [],
@@ -791,9 +802,13 @@ var useMultiSelect = ({
791
802
  }) => {
792
803
  const [state, setState] = (0, import_react4.useState)(initialState);
793
804
  const optionsRef = (0, import_react4.useRef)(options);
805
+ const valuesRef = (0, import_react4.useRef)(value);
794
806
  (0, import_react4.useEffect)(() => {
795
807
  optionsRef.current = options;
796
808
  }, [options]);
809
+ (0, import_react4.useEffect)(() => {
810
+ valuesRef.current = state.values;
811
+ }, [state.values]);
797
812
  (0, import_react4.useEffect)(() => {
798
813
  const selectedItems = options.filter((opt) => value.includes(opt.value));
799
814
  setState((prevState) => ({
@@ -802,9 +817,15 @@ var useMultiSelect = ({
802
817
  selectedItems
803
818
  }));
804
819
  }, [options, value]);
820
+ const getLockedSelectedValues = () => optionsRef.current.filter(
821
+ (opt) => isLockedOption(opt) && valuesRef.current.includes(opt.value)
822
+ ).map((opt) => opt.value);
805
823
  const onChoose = (0, import_react4.useCallback)(
806
824
  (selectedIds) => {
807
- const newValues = optionsRef.current.filter((opt) => selectedIds.includes(String(opt.value))).map((opt) => opt.value);
825
+ const lockedValues = getLockedSelectedValues();
826
+ const newValues = optionsRef.current.filter(
827
+ (opt) => selectedIds.includes(String(opt.value)) || lockedValues.includes(opt.value)
828
+ ).map((opt) => opt.value);
808
829
  const newSelectedItems = optionsRef.current.filter(
809
830
  (opt) => newValues.includes(opt.value)
810
831
  );
@@ -820,6 +841,8 @@ var useMultiSelect = ({
820
841
  const onRemove = (0, import_react4.useCallback)(
821
842
  (value2, event) => {
822
843
  event?.stopPropagation();
844
+ const option = optionsRef.current.find((opt) => opt.value === value2);
845
+ if (option && isLockedOption(option)) return;
823
846
  setState((prev) => {
824
847
  const newValues = prev.values.filter((v) => v !== value2);
825
848
  const newSelectedItems = optionsRef.current.filter(
@@ -836,12 +859,16 @@ var useMultiSelect = ({
836
859
  [onChange]
837
860
  );
838
861
  const onRemoveAll = (0, import_react4.useCallback)(() => {
862
+ const keptValues = getLockedSelectedValues();
863
+ const keptItems = optionsRef.current.filter(
864
+ (opt) => keptValues.includes(opt.value)
865
+ );
839
866
  setState((prevState) => ({
840
867
  ...prevState,
841
- values: [],
842
- selectedItems: []
868
+ values: keptValues,
869
+ selectedItems: keptItems
843
870
  }));
844
- onChange?.([]);
871
+ onChange?.(keptValues);
845
872
  }, [onChange]);
846
873
  const onSelectClick = (0, import_react4.useCallback)(() => {
847
874
  setState((prevState) => ({
@@ -875,6 +902,8 @@ var Portal = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(impo
875
902
  // src/MultiSelect.tsx
876
903
  var import_jsx_runtime7 = require("react/jsx-runtime");
877
904
  var EXTERNAL_MENU_MIN_WIDTH_DEFAULT = 540;
905
+ var SEARCH_ROW_HEIGHT = 60;
906
+ var SELECT_ALL_ROW_HEIGHT = 37;
878
907
  var MultiSelect = (0, import_react5.forwardRef)(
879
908
  ({
880
909
  options,
@@ -901,6 +930,9 @@ var MultiSelect = (0, import_react5.forwardRef)(
901
930
  searchable = false,
902
931
  searchPlaceholder = "Search",
903
932
  noOptionsMessage = "No results",
933
+ selectAll = false,
934
+ selectAllLabel = "Select all",
935
+ deselectAllLabel = "Deselect all",
904
936
  testID,
905
937
  themeMode,
906
938
  themeProductContext
@@ -952,22 +984,33 @@ var MultiSelect = (0, import_react5.forwardRef)(
952
984
  const filteredOptions = searchable && searchValue ? options.filter(
953
985
  (opt) => getOptionText(opt).toLowerCase().includes(searchValue.toLowerCase())
954
986
  ) : options;
987
+ const selectedIds = values.map(String);
955
988
  const menuItems = filteredOptions.map((opt) => {
956
989
  const id = String(opt.value);
957
- const checked = values.map(String).includes(id);
990
+ const checked = selectedIds.includes(id);
958
991
  return {
959
992
  id,
960
993
  children: opt.label,
961
994
  checked,
962
- disabled: opt.disabled
995
+ disabled: opt.disabled,
996
+ locked: opt.removable === false
963
997
  };
964
998
  });
965
999
  const handleItemToggle = (id, checked) => {
966
- const value2 = options.find((opt) => String(opt.value) === id)?.value;
967
- if (value2 === void 0) return;
968
- const newValues = checked ? [...values, value2] : values.filter((v) => v !== value2);
1000
+ const option = options.find((opt) => String(opt.value) === id);
1001
+ if (option === void 0) return;
1002
+ if (!checked && option.removable === false) return;
1003
+ const newValues = checked ? [...values, option.value] : values.filter((v) => v !== option.value);
969
1004
  onChoose(newValues.map(String));
970
1005
  };
1006
+ const selectAllTargets = filteredOptions.filter((opt) => !opt.disabled);
1007
+ const showSelectAll = selectAll && dropdownMenu && selectAllTargets.length > 0;
1008
+ const areAllTargetsSelected = showSelectAll && selectAllTargets.every((opt) => selectedIds.includes(String(opt.value)));
1009
+ const handleSelectAllToggle = () => {
1010
+ const targetIds = selectAllTargets.map((opt) => String(opt.value));
1011
+ const nextIds = areAllTargetsSelected ? selectedIds.filter((id) => !targetIds.includes(id)) : Array.from(/* @__PURE__ */ new Set([...selectedIds, ...targetIds]));
1012
+ onChoose(nextIds);
1013
+ };
971
1014
  const controlMenuOpen = dropdownMenu ? isOpen : Boolean(menuOpen);
972
1015
  const controlOnClick = dropdownMenu ? onSelectClick : onTriggerPress;
973
1016
  const isScrollOverflow = optionOverflow === "scroll";
@@ -986,6 +1029,7 @@ var MultiSelect = (0, import_react5.forwardRef)(
986
1029
  overflow: "hidden",
987
1030
  textOverflow: "ellipsis"
988
1031
  };
1032
+ const optionsListMaxHeight = maxHeight - (searchable ? SEARCH_ROW_HEIGHT : 0) - (showSelectAll ? SELECT_ALL_ROW_HEIGHT : 0);
989
1033
  (0, import_react5.useEffect)(() => {
990
1034
  if (!isWeb || !controlMenuOpen || isDisable || !dropdownMenu) return;
991
1035
  let rafId = null;
@@ -1119,12 +1163,41 @@ var MultiSelect = (0, import_react5.forwardRef)(
1119
1163
  )
1120
1164
  }
1121
1165
  ),
1166
+ showSelectAll && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1167
+ Box,
1168
+ {
1169
+ role: "button",
1170
+ "aria-label": areAllTargetsSelected ? deselectAllLabel : selectAllLabel,
1171
+ testID: testID ? `${testID}-select-all` : void 0,
1172
+ paddingHorizontal: sizeStyles.paddingHorizontal,
1173
+ paddingVertical: 8,
1174
+ borderBottomWidth: 1,
1175
+ borderColor: theme.colors.border.secondary,
1176
+ flexDirection: "row",
1177
+ alignItems: "center",
1178
+ onPress: handleSelectAllToggle,
1179
+ hoverStyle: {
1180
+ backgroundColor: theme.colors.control.input.bgHover
1181
+ },
1182
+ style: { cursor: "pointer" },
1183
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1184
+ Text,
1185
+ {
1186
+ color: theme.colors.content.brand.primary,
1187
+ fontSize: sizeStyles.fontSize,
1188
+ fontWeight: "500",
1189
+ numberOfLines: 1,
1190
+ children: areAllTargetsSelected ? deselectAllLabel : selectAllLabel
1191
+ }
1192
+ )
1193
+ }
1194
+ ),
1122
1195
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1123
1196
  Box,
1124
1197
  {
1125
1198
  paddingVertical: 4,
1126
1199
  style: {
1127
- maxHeight: searchable ? maxHeight - 60 : maxHeight,
1200
+ maxHeight: optionsListMaxHeight,
1128
1201
  overflowY: "auto",
1129
1202
  overflowX: optionListOverflowX,
1130
1203
  scrollbarWidth: "none"
@@ -1147,6 +1220,7 @@ var MultiSelect = (0, import_react5.forwardRef)(
1147
1220
  ) : menuItems.map((item, _index) => {
1148
1221
  const brandColors = theme.colors.control.brand.primary;
1149
1222
  const contentColors = theme.colors.content;
1223
+ const isInert = item.locked && item.checked;
1150
1224
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1151
1225
  Box,
1152
1226
  {
@@ -1154,7 +1228,7 @@ var MultiSelect = (0, import_react5.forwardRef)(
1154
1228
  paddingHorizontal: sizeStyles.paddingHorizontal,
1155
1229
  paddingVertical: 8,
1156
1230
  onPress: () => {
1157
- if (!item.disabled) {
1231
+ if (!item.disabled && !isInert) {
1158
1232
  handleItemToggle(item.id, !item.checked);
1159
1233
  }
1160
1234
  },
@@ -1166,7 +1240,7 @@ var MultiSelect = (0, import_react5.forwardRef)(
1166
1240
  backgroundColor: theme.colors.control.input.bgHover
1167
1241
  } : void 0,
1168
1242
  style: {
1169
- cursor: item.disabled ? "not-allowed" : "pointer",
1243
+ cursor: item.disabled ? "not-allowed" : isInert ? "default" : "pointer",
1170
1244
  opacity: item.disabled ? 0.5 : 1,
1171
1245
  minWidth: optionRowMinWidth
1172
1246
  },