@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/web/index.js CHANGED
@@ -464,7 +464,8 @@ var useMultiSelectControl = ({
464
464
  stateList: stateList.map((item) => item.value),
465
465
  variant,
466
466
  size,
467
- removeTagsButtons
467
+ removeTagsButtons,
468
+ lockedList: stateList.filter((item) => item.removable === false).map((item) => item.value)
468
469
  }),
469
470
  [stateList, variant, size, removeTagsButtons]
470
471
  );
@@ -477,11 +478,18 @@ var useMultiSelectControl = ({
477
478
  return true;
478
479
  if (prev.stateList.length !== widthsDependencies.stateList.length)
479
480
  return true;
481
+ if (prev.lockedList.length !== widthsDependencies.lockedList.length)
482
+ return true;
480
483
  let i = 0;
481
484
  for (const v of prev.stateList) {
482
485
  if (v !== widthsDependencies.stateList[i]) return true;
483
486
  i++;
484
487
  }
488
+ let j = 0;
489
+ for (const v of prev.lockedList) {
490
+ if (v !== widthsDependencies.lockedList[j]) return true;
491
+ j++;
492
+ }
485
493
  return false;
486
494
  };
487
495
  const setPlaceholder = () => {
@@ -535,26 +543,28 @@ var useMultiSelectControl = ({
535
543
  }
536
544
  const content = displayedItems.map((item, index2) => {
537
545
  const isCount = item.value === COUNT_ITEM_VALUE;
546
+ const isLocked = !isCount && item.removable === false;
547
+ const canRemove = !isCount && !isLocked;
538
548
  const isReducable = index2 === 0 && displayStateRef.current === 3 /* Render */ && displayedItemsCount === 0 || flexible;
539
549
  if (variant === "tag") {
540
550
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
541
551
  Box,
542
552
  {
543
- onPress: !isCount ? (e) => {
553
+ onPress: canRemove ? (e) => {
544
554
  e.stopPropagation();
545
555
  if (!disabled) removeValue(item.value, e);
546
556
  } : void 0,
547
557
  style: {
548
558
  minWidth: isReducable ? 0 : "fit-content",
549
559
  opacity: disabled ? 0.5 : 1,
550
- cursor: disabled || isCount ? "not-allowed" : "pointer"
560
+ cursor: isLocked ? "default" : disabled || isCount ? "not-allowed" : "pointer"
551
561
  },
552
562
  children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
553
563
  import_xui_tag.Tag,
554
564
  {
555
565
  size,
556
566
  tone: "primary",
557
- onRemove: removeTagsButtons && !isCount ? (e) => {
567
+ onRemove: removeTagsButtons && canRemove ? (e) => {
558
568
  e?.stopPropagation();
559
569
  if (!disabled) removeValue(item.value, e);
560
570
  } : void 0,
@@ -813,6 +823,7 @@ var initialState = {
813
823
  selectedItems: [],
814
824
  isOpen: false
815
825
  };
826
+ var isLockedOption = (option) => option.removable === false;
816
827
  var useMultiSelect = ({
817
828
  options,
818
829
  value = [],
@@ -820,9 +831,13 @@ var useMultiSelect = ({
820
831
  }) => {
821
832
  const [state, setState] = (0, import_react5.useState)(initialState);
822
833
  const optionsRef = (0, import_react5.useRef)(options);
834
+ const valuesRef = (0, import_react5.useRef)(value);
823
835
  (0, import_react5.useEffect)(() => {
824
836
  optionsRef.current = options;
825
837
  }, [options]);
838
+ (0, import_react5.useEffect)(() => {
839
+ valuesRef.current = state.values;
840
+ }, [state.values]);
826
841
  (0, import_react5.useEffect)(() => {
827
842
  const selectedItems = options.filter((opt) => value.includes(opt.value));
828
843
  setState((prevState) => ({
@@ -831,9 +846,15 @@ var useMultiSelect = ({
831
846
  selectedItems
832
847
  }));
833
848
  }, [options, value]);
849
+ const getLockedSelectedValues = () => optionsRef.current.filter(
850
+ (opt) => isLockedOption(opt) && valuesRef.current.includes(opt.value)
851
+ ).map((opt) => opt.value);
834
852
  const onChoose = (0, import_react5.useCallback)(
835
853
  (selectedIds) => {
836
- const newValues = optionsRef.current.filter((opt) => selectedIds.includes(String(opt.value))).map((opt) => opt.value);
854
+ const lockedValues = getLockedSelectedValues();
855
+ const newValues = optionsRef.current.filter(
856
+ (opt) => selectedIds.includes(String(opt.value)) || lockedValues.includes(opt.value)
857
+ ).map((opt) => opt.value);
837
858
  const newSelectedItems = optionsRef.current.filter(
838
859
  (opt) => newValues.includes(opt.value)
839
860
  );
@@ -849,6 +870,8 @@ var useMultiSelect = ({
849
870
  const onRemove = (0, import_react5.useCallback)(
850
871
  (value2, event) => {
851
872
  event?.stopPropagation();
873
+ const option = optionsRef.current.find((opt) => opt.value === value2);
874
+ if (option && isLockedOption(option)) return;
852
875
  setState((prev) => {
853
876
  const newValues = prev.values.filter((v) => v !== value2);
854
877
  const newSelectedItems = optionsRef.current.filter(
@@ -865,12 +888,16 @@ var useMultiSelect = ({
865
888
  [onChange]
866
889
  );
867
890
  const onRemoveAll = (0, import_react5.useCallback)(() => {
891
+ const keptValues = getLockedSelectedValues();
892
+ const keptItems = optionsRef.current.filter(
893
+ (opt) => keptValues.includes(opt.value)
894
+ );
868
895
  setState((prevState) => ({
869
896
  ...prevState,
870
- values: [],
871
- selectedItems: []
897
+ values: keptValues,
898
+ selectedItems: keptItems
872
899
  }));
873
- onChange?.([]);
900
+ onChange?.(keptValues);
874
901
  }, [onChange]);
875
902
  const onSelectClick = (0, import_react5.useCallback)(() => {
876
903
  setState((prevState) => ({
@@ -910,6 +937,8 @@ var Portal = ({ children }) => {
910
937
  // src/MultiSelect.tsx
911
938
  var import_jsx_runtime6 = require("react/jsx-runtime");
912
939
  var EXTERNAL_MENU_MIN_WIDTH_DEFAULT = 540;
940
+ var SEARCH_ROW_HEIGHT = 60;
941
+ var SELECT_ALL_ROW_HEIGHT = 37;
913
942
  var MultiSelect = (0, import_react7.forwardRef)(
914
943
  ({
915
944
  options,
@@ -936,6 +965,9 @@ var MultiSelect = (0, import_react7.forwardRef)(
936
965
  searchable = false,
937
966
  searchPlaceholder = "Search",
938
967
  noOptionsMessage = "No results",
968
+ selectAll = false,
969
+ selectAllLabel = "Select all",
970
+ deselectAllLabel = "Deselect all",
939
971
  testID,
940
972
  themeMode,
941
973
  themeProductContext
@@ -987,22 +1019,33 @@ var MultiSelect = (0, import_react7.forwardRef)(
987
1019
  const filteredOptions = searchable && searchValue ? options.filter(
988
1020
  (opt) => getOptionText(opt).toLowerCase().includes(searchValue.toLowerCase())
989
1021
  ) : options;
1022
+ const selectedIds = values.map(String);
990
1023
  const menuItems = filteredOptions.map((opt) => {
991
1024
  const id = String(opt.value);
992
- const checked = values.map(String).includes(id);
1025
+ const checked = selectedIds.includes(id);
993
1026
  return {
994
1027
  id,
995
1028
  children: opt.label,
996
1029
  checked,
997
- disabled: opt.disabled
1030
+ disabled: opt.disabled,
1031
+ locked: opt.removable === false
998
1032
  };
999
1033
  });
1000
1034
  const handleItemToggle = (id, checked) => {
1001
- const value2 = options.find((opt) => String(opt.value) === id)?.value;
1002
- if (value2 === void 0) return;
1003
- const newValues = checked ? [...values, value2] : values.filter((v) => v !== value2);
1035
+ const option = options.find((opt) => String(opt.value) === id);
1036
+ if (option === void 0) return;
1037
+ if (!checked && option.removable === false) return;
1038
+ const newValues = checked ? [...values, option.value] : values.filter((v) => v !== option.value);
1004
1039
  onChoose(newValues.map(String));
1005
1040
  };
1041
+ const selectAllTargets = filteredOptions.filter((opt) => !opt.disabled);
1042
+ const showSelectAll = selectAll && dropdownMenu && selectAllTargets.length > 0;
1043
+ const areAllTargetsSelected = showSelectAll && selectAllTargets.every((opt) => selectedIds.includes(String(opt.value)));
1044
+ const handleSelectAllToggle = () => {
1045
+ const targetIds = selectAllTargets.map((opt) => String(opt.value));
1046
+ const nextIds = areAllTargetsSelected ? selectedIds.filter((id) => !targetIds.includes(id)) : Array.from(/* @__PURE__ */ new Set([...selectedIds, ...targetIds]));
1047
+ onChoose(nextIds);
1048
+ };
1006
1049
  const controlMenuOpen = dropdownMenu ? isOpen : Boolean(menuOpen);
1007
1050
  const controlOnClick = dropdownMenu ? onSelectClick : onTriggerPress;
1008
1051
  const isScrollOverflow = optionOverflow === "scroll";
@@ -1021,6 +1064,7 @@ var MultiSelect = (0, import_react7.forwardRef)(
1021
1064
  overflow: "hidden",
1022
1065
  textOverflow: "ellipsis"
1023
1066
  };
1067
+ const optionsListMaxHeight = maxHeight - (searchable ? SEARCH_ROW_HEIGHT : 0) - (showSelectAll ? SELECT_ALL_ROW_HEIGHT : 0);
1024
1068
  (0, import_react7.useEffect)(() => {
1025
1069
  if (!isWeb || !controlMenuOpen || isDisable || !dropdownMenu) return;
1026
1070
  let rafId = null;
@@ -1154,12 +1198,41 @@ var MultiSelect = (0, import_react7.forwardRef)(
1154
1198
  )
1155
1199
  }
1156
1200
  ),
1201
+ showSelectAll && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1202
+ Box,
1203
+ {
1204
+ role: "button",
1205
+ "aria-label": areAllTargetsSelected ? deselectAllLabel : selectAllLabel,
1206
+ testID: testID ? `${testID}-select-all` : void 0,
1207
+ paddingHorizontal: sizeStyles.paddingHorizontal,
1208
+ paddingVertical: 8,
1209
+ borderBottomWidth: 1,
1210
+ borderColor: theme.colors.border.secondary,
1211
+ flexDirection: "row",
1212
+ alignItems: "center",
1213
+ onPress: handleSelectAllToggle,
1214
+ hoverStyle: {
1215
+ backgroundColor: theme.colors.control.input.bgHover
1216
+ },
1217
+ style: { cursor: "pointer" },
1218
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1219
+ Text,
1220
+ {
1221
+ color: theme.colors.content.brand.primary,
1222
+ fontSize: sizeStyles.fontSize,
1223
+ fontWeight: "500",
1224
+ numberOfLines: 1,
1225
+ children: areAllTargetsSelected ? deselectAllLabel : selectAllLabel
1226
+ }
1227
+ )
1228
+ }
1229
+ ),
1157
1230
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1158
1231
  Box,
1159
1232
  {
1160
1233
  paddingVertical: 4,
1161
1234
  style: {
1162
- maxHeight: searchable ? maxHeight - 60 : maxHeight,
1235
+ maxHeight: optionsListMaxHeight,
1163
1236
  overflowY: "auto",
1164
1237
  overflowX: optionListOverflowX,
1165
1238
  scrollbarWidth: "none"
@@ -1182,6 +1255,7 @@ var MultiSelect = (0, import_react7.forwardRef)(
1182
1255
  ) : menuItems.map((item, _index) => {
1183
1256
  const brandColors = theme.colors.control.brand.primary;
1184
1257
  const contentColors = theme.colors.content;
1258
+ const isInert = item.locked && item.checked;
1185
1259
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1186
1260
  Box,
1187
1261
  {
@@ -1189,7 +1263,7 @@ var MultiSelect = (0, import_react7.forwardRef)(
1189
1263
  paddingHorizontal: sizeStyles.paddingHorizontal,
1190
1264
  paddingVertical: 8,
1191
1265
  onPress: () => {
1192
- if (!item.disabled) {
1266
+ if (!item.disabled && !isInert) {
1193
1267
  handleItemToggle(item.id, !item.checked);
1194
1268
  }
1195
1269
  },
@@ -1201,7 +1275,7 @@ var MultiSelect = (0, import_react7.forwardRef)(
1201
1275
  backgroundColor: theme.colors.control.input.bgHover
1202
1276
  } : void 0,
1203
1277
  style: {
1204
- cursor: item.disabled ? "not-allowed" : "pointer",
1278
+ cursor: item.disabled ? "not-allowed" : isInert ? "default" : "pointer",
1205
1279
  opacity: item.disabled ? 0.5 : 1,
1206
1280
  minWidth: optionRowMinWidth
1207
1281
  },