@xsolla/xui-multi-select 0.216.1 → 0.218.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/web/index.mjs CHANGED
@@ -448,7 +448,8 @@ var useMultiSelectControl = ({
448
448
  stateList: stateList.map((item) => item.value),
449
449
  variant,
450
450
  size,
451
- removeTagsButtons
451
+ removeTagsButtons,
452
+ lockedList: stateList.filter((item) => item.removable === false).map((item) => item.value)
452
453
  }),
453
454
  [stateList, variant, size, removeTagsButtons]
454
455
  );
@@ -461,11 +462,18 @@ var useMultiSelectControl = ({
461
462
  return true;
462
463
  if (prev.stateList.length !== widthsDependencies.stateList.length)
463
464
  return true;
465
+ if (prev.lockedList.length !== widthsDependencies.lockedList.length)
466
+ return true;
464
467
  let i = 0;
465
468
  for (const v of prev.stateList) {
466
469
  if (v !== widthsDependencies.stateList[i]) return true;
467
470
  i++;
468
471
  }
472
+ let j = 0;
473
+ for (const v of prev.lockedList) {
474
+ if (v !== widthsDependencies.lockedList[j]) return true;
475
+ j++;
476
+ }
469
477
  return false;
470
478
  };
471
479
  const setPlaceholder = () => {
@@ -519,26 +527,28 @@ var useMultiSelectControl = ({
519
527
  }
520
528
  const content = displayedItems.map((item, index2) => {
521
529
  const isCount = item.value === COUNT_ITEM_VALUE;
530
+ const isLocked = !isCount && item.removable === false;
531
+ const canRemove = !isCount && !isLocked;
522
532
  const isReducable = index2 === 0 && displayStateRef.current === 3 /* Render */ && displayedItemsCount === 0 || flexible;
523
533
  if (variant === "tag") {
524
534
  return /* @__PURE__ */ jsx4(
525
535
  Box,
526
536
  {
527
- onPress: !isCount ? (e) => {
537
+ onPress: canRemove ? (e) => {
528
538
  e.stopPropagation();
529
539
  if (!disabled) removeValue(item.value, e);
530
540
  } : void 0,
531
541
  style: {
532
542
  minWidth: isReducable ? 0 : "fit-content",
533
543
  opacity: disabled ? 0.5 : 1,
534
- cursor: disabled || isCount ? "not-allowed" : "pointer"
544
+ cursor: isLocked ? "default" : disabled || isCount ? "not-allowed" : "pointer"
535
545
  },
536
546
  children: /* @__PURE__ */ jsx4(
537
547
  Tag,
538
548
  {
539
549
  size,
540
550
  tone: "primary",
541
- onRemove: removeTagsButtons && !isCount ? (e) => {
551
+ onRemove: removeTagsButtons && canRemove ? (e) => {
542
552
  e?.stopPropagation();
543
553
  if (!disabled) removeValue(item.value, e);
544
554
  } : void 0,
@@ -797,6 +807,7 @@ var initialState = {
797
807
  selectedItems: [],
798
808
  isOpen: false
799
809
  };
810
+ var isLockedOption = (option) => option.removable === false;
800
811
  var useMultiSelect = ({
801
812
  options,
802
813
  value = [],
@@ -804,9 +815,13 @@ var useMultiSelect = ({
804
815
  }) => {
805
816
  const [state, setState] = useState2(initialState);
806
817
  const optionsRef = useRef3(options);
818
+ const valuesRef = useRef3(value);
807
819
  useEffect2(() => {
808
820
  optionsRef.current = options;
809
821
  }, [options]);
822
+ useEffect2(() => {
823
+ valuesRef.current = state.values;
824
+ }, [state.values]);
810
825
  useEffect2(() => {
811
826
  const selectedItems = options.filter((opt) => value.includes(opt.value));
812
827
  setState((prevState) => ({
@@ -815,9 +830,15 @@ var useMultiSelect = ({
815
830
  selectedItems
816
831
  }));
817
832
  }, [options, value]);
833
+ const getLockedSelectedValues = () => optionsRef.current.filter(
834
+ (opt) => isLockedOption(opt) && valuesRef.current.includes(opt.value)
835
+ ).map((opt) => opt.value);
818
836
  const onChoose = useCallback(
819
837
  (selectedIds) => {
820
- const newValues = optionsRef.current.filter((opt) => selectedIds.includes(String(opt.value))).map((opt) => opt.value);
838
+ const lockedValues = getLockedSelectedValues();
839
+ const newValues = optionsRef.current.filter(
840
+ (opt) => selectedIds.includes(String(opt.value)) || lockedValues.includes(opt.value)
841
+ ).map((opt) => opt.value);
821
842
  const newSelectedItems = optionsRef.current.filter(
822
843
  (opt) => newValues.includes(opt.value)
823
844
  );
@@ -833,6 +854,8 @@ var useMultiSelect = ({
833
854
  const onRemove = useCallback(
834
855
  (value2, event) => {
835
856
  event?.stopPropagation();
857
+ const option = optionsRef.current.find((opt) => opt.value === value2);
858
+ if (option && isLockedOption(option)) return;
836
859
  setState((prev) => {
837
860
  const newValues = prev.values.filter((v) => v !== value2);
838
861
  const newSelectedItems = optionsRef.current.filter(
@@ -849,12 +872,16 @@ var useMultiSelect = ({
849
872
  [onChange]
850
873
  );
851
874
  const onRemoveAll = useCallback(() => {
875
+ const keptValues = getLockedSelectedValues();
876
+ const keptItems = optionsRef.current.filter(
877
+ (opt) => keptValues.includes(opt.value)
878
+ );
852
879
  setState((prevState) => ({
853
880
  ...prevState,
854
- values: [],
855
- selectedItems: []
881
+ values: keptValues,
882
+ selectedItems: keptItems
856
883
  }));
857
- onChange?.([]);
884
+ onChange?.(keptValues);
858
885
  }, [onChange]);
859
886
  const onSelectClick = useCallback(() => {
860
887
  setState((prevState) => ({
@@ -894,6 +921,8 @@ var Portal = ({ children }) => {
894
921
  // src/MultiSelect.tsx
895
922
  import { Fragment, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
896
923
  var EXTERNAL_MENU_MIN_WIDTH_DEFAULT = 540;
924
+ var SEARCH_ROW_HEIGHT = 60;
925
+ var SELECT_ALL_ROW_HEIGHT = 37;
897
926
  var MultiSelect = forwardRef2(
898
927
  ({
899
928
  options,
@@ -920,6 +949,9 @@ var MultiSelect = forwardRef2(
920
949
  searchable = false,
921
950
  searchPlaceholder = "Search",
922
951
  noOptionsMessage = "No results",
952
+ selectAll = false,
953
+ selectAllLabel = "Select all",
954
+ deselectAllLabel = "Deselect all",
923
955
  testID,
924
956
  themeMode,
925
957
  themeProductContext
@@ -971,22 +1003,33 @@ var MultiSelect = forwardRef2(
971
1003
  const filteredOptions = searchable && searchValue ? options.filter(
972
1004
  (opt) => getOptionText(opt).toLowerCase().includes(searchValue.toLowerCase())
973
1005
  ) : options;
1006
+ const selectedIds = values.map(String);
974
1007
  const menuItems = filteredOptions.map((opt) => {
975
1008
  const id = String(opt.value);
976
- const checked = values.map(String).includes(id);
1009
+ const checked = selectedIds.includes(id);
977
1010
  return {
978
1011
  id,
979
1012
  children: opt.label,
980
1013
  checked,
981
- disabled: opt.disabled
1014
+ disabled: opt.disabled,
1015
+ locked: opt.removable === false
982
1016
  };
983
1017
  });
984
1018
  const handleItemToggle = (id, checked) => {
985
- const value2 = options.find((opt) => String(opt.value) === id)?.value;
986
- if (value2 === void 0) return;
987
- const newValues = checked ? [...values, value2] : values.filter((v) => v !== value2);
1019
+ const option = options.find((opt) => String(opt.value) === id);
1020
+ if (option === void 0) return;
1021
+ if (!checked && option.removable === false) return;
1022
+ const newValues = checked ? [...values, option.value] : values.filter((v) => v !== option.value);
988
1023
  onChoose(newValues.map(String));
989
1024
  };
1025
+ const selectAllTargets = filteredOptions.filter((opt) => !opt.disabled);
1026
+ const showSelectAll = selectAll && dropdownMenu && selectAllTargets.length > 0;
1027
+ const areAllTargetsSelected = showSelectAll && selectAllTargets.every((opt) => selectedIds.includes(String(opt.value)));
1028
+ const handleSelectAllToggle = () => {
1029
+ const targetIds = selectAllTargets.map((opt) => String(opt.value));
1030
+ const nextIds = areAllTargetsSelected ? selectedIds.filter((id) => !targetIds.includes(id)) : Array.from(/* @__PURE__ */ new Set([...selectedIds, ...targetIds]));
1031
+ onChoose(nextIds);
1032
+ };
990
1033
  const controlMenuOpen = dropdownMenu ? isOpen : Boolean(menuOpen);
991
1034
  const controlOnClick = dropdownMenu ? onSelectClick : onTriggerPress;
992
1035
  const isScrollOverflow = optionOverflow === "scroll";
@@ -1005,6 +1048,7 @@ var MultiSelect = forwardRef2(
1005
1048
  overflow: "hidden",
1006
1049
  textOverflow: "ellipsis"
1007
1050
  };
1051
+ const optionsListMaxHeight = maxHeight - (searchable ? SEARCH_ROW_HEIGHT : 0) - (showSelectAll ? SELECT_ALL_ROW_HEIGHT : 0);
1008
1052
  useEffect4(() => {
1009
1053
  if (!isWeb || !controlMenuOpen || isDisable || !dropdownMenu) return;
1010
1054
  let rafId = null;
@@ -1138,12 +1182,41 @@ var MultiSelect = forwardRef2(
1138
1182
  )
1139
1183
  }
1140
1184
  ),
1185
+ showSelectAll && /* @__PURE__ */ jsx6(
1186
+ Box,
1187
+ {
1188
+ role: "button",
1189
+ "aria-label": areAllTargetsSelected ? deselectAllLabel : selectAllLabel,
1190
+ testID: testID ? `${testID}-select-all` : void 0,
1191
+ paddingHorizontal: sizeStyles.paddingHorizontal,
1192
+ paddingVertical: 8,
1193
+ borderBottomWidth: 1,
1194
+ borderColor: theme.colors.border.secondary,
1195
+ flexDirection: "row",
1196
+ alignItems: "center",
1197
+ onPress: handleSelectAllToggle,
1198
+ hoverStyle: {
1199
+ backgroundColor: theme.colors.control.input.bgHover
1200
+ },
1201
+ style: { cursor: "pointer" },
1202
+ children: /* @__PURE__ */ jsx6(
1203
+ Text,
1204
+ {
1205
+ color: theme.colors.content.brand.primary,
1206
+ fontSize: sizeStyles.fontSize,
1207
+ fontWeight: "500",
1208
+ numberOfLines: 1,
1209
+ children: areAllTargetsSelected ? deselectAllLabel : selectAllLabel
1210
+ }
1211
+ )
1212
+ }
1213
+ ),
1141
1214
  /* @__PURE__ */ jsx6(
1142
1215
  Box,
1143
1216
  {
1144
1217
  paddingVertical: 4,
1145
1218
  style: {
1146
- maxHeight: searchable ? maxHeight - 60 : maxHeight,
1219
+ maxHeight: optionsListMaxHeight,
1147
1220
  overflowY: "auto",
1148
1221
  overflowX: optionListOverflowX,
1149
1222
  scrollbarWidth: "none"
@@ -1166,6 +1239,7 @@ var MultiSelect = forwardRef2(
1166
1239
  ) : menuItems.map((item, _index) => {
1167
1240
  const brandColors = theme.colors.control.brand.primary;
1168
1241
  const contentColors = theme.colors.content;
1242
+ const isInert = item.locked && item.checked;
1169
1243
  return /* @__PURE__ */ jsx6(
1170
1244
  Box,
1171
1245
  {
@@ -1173,7 +1247,7 @@ var MultiSelect = forwardRef2(
1173
1247
  paddingHorizontal: sizeStyles.paddingHorizontal,
1174
1248
  paddingVertical: 8,
1175
1249
  onPress: () => {
1176
- if (!item.disabled) {
1250
+ if (!item.disabled && !isInert) {
1177
1251
  handleItemToggle(item.id, !item.checked);
1178
1252
  }
1179
1253
  },
@@ -1185,7 +1259,7 @@ var MultiSelect = forwardRef2(
1185
1259
  backgroundColor: theme.colors.control.input.bgHover
1186
1260
  } : void 0,
1187
1261
  style: {
1188
- cursor: item.disabled ? "not-allowed" : "pointer",
1262
+ cursor: item.disabled ? "not-allowed" : isInert ? "default" : "pointer",
1189
1263
  opacity: item.disabled ? 0.5 : 1,
1190
1264
  minWidth: optionRowMinWidth
1191
1265
  },