@procore/core-react
Version:
React library of Procore Design Guidelines
1,162 lines (1,130 loc) • 55.6 kB
JavaScript
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; }
function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
import { autoUpdate, flip, offset, size, useClick, useDismiss, useFloating, useInteractions, useListNavigation } from '@floating-ui/react';
import { useId } from '@react-aria/utils';
import debounce from 'lodash.debounce';
import React from 'react';
import { ulid } from 'ulid';
import { useI18nContext } from '../_hooks/I18n';
import { useZIndexContext } from '../_hooks/ZIndex';
import { spacing } from '../_styles/spacing';
import * as defaultComponents from './SuperSelect.components';
import { draggableOptionIdSymbol, isOptSelectAllSymbol } from './SuperSelect.constants';
import { extendedSelectMenuWidth } from './SuperSelect.styles';
import { collectGroupsInOrderOfOccurrence, createOptgroup, defaultOnUnselectAll, getBatchOptionFormatter, getIsAllOptionsUngrouped, getOptionIsOptgroup, getOptionIsOptSelectAll, getOptionsSortingAlgorithm, isMultiple, removeEmptyOptGroups, reorder, sortOptgroups } from './SuperSelect.utils';
var listContainerVerticalPadding = spacing.sm * 2;
/* Based on item height + padding (~35px), this minimum height is enough to show 4-6 items before
* the overflow is hidden before scrolling. This will ensure the dropdown doesn't open with minimal
* height, which would lead to a poor scrolling experience. */
var reasonableDropdownMinimumHeight = 248;
function noop() {}
function defaultGetOptionValue(option) {
return option === null || option === void 0 ? void 0 : option.value;
}
function defaultGetOptionIsBatch(option) {
return Array.isArray(option === null || option === void 0 ? void 0 : option.value);
}
function defaultGetOptionIsDisabled(option) {
var _option$disabled;
return (_option$disabled = option === null || option === void 0 ? void 0 : option.disabled) !== null && _option$disabled !== void 0 ? _option$disabled : false;
}
function defaultGetOptionGroup(option) {
var _option$group;
return (_option$group = option === null || option === void 0 ? void 0 : option.group) !== null && _option$group !== void 0 ? _option$group : '';
}
function defaultSetOptionGroup(option, group) {
return _objectSpread(_objectSpread({}, option), {}, {
group: group
});
}
function defaultGetOptionLabel(option) {
var _option$label;
return (_option$label = option === null || option === void 0 ? void 0 : option.label) !== null && _option$label !== void 0 ? _option$label : '';
}
function stringContains(str1, str2) {
return str1.toLowerCase().includes(str2.toLowerCase());
}
function useTokenNavigation(context) {
var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref$enabled = _ref.enabled,
enabled = _ref$enabled === void 0 ? true : _ref$enabled,
_ref$value = _ref.value,
value = _ref$value === void 0 ? [] : _ref$value,
tokenRemoveRefs = _ref.tokenRemoveRefs;
return {
reference: {
onKeyDown: function onKeyDown(e) {
if (!enabled || !Array.isArray(value) || value.length === 0) {
return;
}
if (e.key === 'ArrowLeft') {
var _tokenRemoveRefs$curr;
e.preventDefault();
tokenRemoveRefs === null || tokenRemoveRefs === void 0 ? void 0 : (_tokenRemoveRefs$curr = tokenRemoveRefs.current[value.length - 1]) === null || _tokenRemoveRefs$curr === void 0 ? void 0 : _tokenRemoveRefs$curr.focus();
}
}
}
};
}
function useKeyboardSelection(context) {
var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref2$enabled = _ref2.enabled,
enabled = _ref2$enabled === void 0 ? true : _ref2$enabled,
_ref2$onSelect = _ref2.onSelect,
onSelect = _ref2$onSelect === void 0 ? function () {} : _ref2$onSelect;
function onKeyDown(e) {
if (!enabled) {
return;
}
if (e.key === 'Enter') {
onSelect();
}
}
return {
reference: {
onKeyDown: onKeyDown
},
floating: {
onKeyDown: onKeyDown
}
};
}
export function useSuperSelect(_ref3) {
var _ref3$block = _ref3.block,
block = _ref3$block === void 0 ? false : _ref3$block,
customComponents = _ref3.components,
defaultValue = _ref3.defaultValue,
_ref3$disabled = _ref3.disabled,
disabled = _ref3$disabled === void 0 ? false : _ref3$disabled,
_ref3$draggable = _ref3.draggable,
draggable = _ref3$draggable === void 0 ? false : _ref3$draggable,
_ref3$emptyMessage = _ref3.emptyMessage,
emptyMessage = _ref3$emptyMessage === void 0 ? 'No results' : _ref3$emptyMessage,
_ref3$selectAllEnable = _ref3.selectAllEnabled,
selectAllEnabled = _ref3$selectAllEnable === void 0 ? false : _ref3$selectAllEnable,
error = _ref3.error,
footer = _ref3.footer,
_ref3$getOptionGroup = _ref3.getOptionGroup,
getOptionGroup = _ref3$getOptionGroup === void 0 ? defaultGetOptionGroup : _ref3$getOptionGroup,
_ref3$getOptionIsBatc = _ref3.getOptionIsBatch,
getOptionIsBatch = _ref3$getOptionIsBatc === void 0 ? defaultGetOptionIsBatch : _ref3$getOptionIsBatc,
_ref3$getOptionIsDisa = _ref3.getOptionIsDisabled,
getOptionIsDisabled = _ref3$getOptionIsDisa === void 0 ? defaultGetOptionIsDisabled : _ref3$getOptionIsDisa,
_ref3$getOptionLabel = _ref3.getOptionLabel,
getOptionLabel = _ref3$getOptionLabel === void 0 ? defaultGetOptionLabel : _ref3$getOptionLabel,
getOptionIsPartiallySelected = _ref3.getOptionIsPartiallySelected,
getOptionIsSelected = _ref3.getOptionIsSelected,
_ref3$getOptionValue = _ref3.getOptionValue,
getOptionValue = _ref3$getOptionValue === void 0 ? defaultGetOptionValue : _ref3$getOptionValue,
_ref3$onScrollBottom = _ref3.onScrollBottom,
onScrollBottom = _ref3$onScrollBottom === void 0 ? noop : _ref3$onScrollBottom,
_ref3$onOpenChange = _ref3.onOpenChange,
_onOpenChange = _ref3$onOpenChange === void 0 ? noop : _ref3$onOpenChange,
header = _ref3.header,
_ref3$loading = _ref3.loading,
loading = _ref3$loading === void 0 ? false : _ref3$loading,
_ref3$multiple = _ref3.multiple,
multiple = _ref3$multiple === void 0 ? false : _ref3$multiple,
onChange = _ref3.onChange,
onManualSort = _ref3.onManualSort,
_ref3$onUnselectAll = _ref3.onUnselectAll,
onUnselectAll = _ref3$onUnselectAll === void 0 ? defaultOnUnselectAll : _ref3$onUnselectAll,
onSelectAll = _ref3.onSelectAll,
_ref3$options = _ref3.options,
sourceOptions = _ref3$options === void 0 ? [] : _ref3$options,
placeholder = _ref3.placeholder,
_ref3$preset = _ref3.preset,
preset = _ref3$preset === void 0 ? '' : _ref3$preset,
presetProps = _ref3.presetProps,
_ref3$search = _ref3.search,
search = _ref3$search === void 0 ? true : _ref3$search,
_ref3$selectionStyle = _ref3.selectionStyle,
selectionStyle = _ref3$selectionStyle === void 0 ? 'highlight' : _ref3$selectionStyle,
_ref3$setOptionGroup = _ref3.setOptionGroup,
setOptionGroup = _ref3$setOptionGroup === void 0 ? defaultSetOptionGroup : _ref3$setOptionGroup,
_ref3$sort = _ref3.sort,
sort = _ref3$sort === void 0 ? true : _ref3$sort,
_ref3$tabIndex = _ref3.tabIndex,
tabIndex = _ref3$tabIndex === void 0 ? 0 : _ref3$tabIndex,
value_ = _ref3.value,
_ref3$overlayMatchesT = _ref3.overlayMatchesTriggerWidth,
overlayMatchesTriggerWidth = _ref3$overlayMatchesT === void 0 ? true : _ref3$overlayMatchesT,
ariaLabel = _ref3['aria-label'],
ariaLabelledBy = _ref3['aria-labelledby'];
React.useEffect(function () {
if (!draggable) {
return;
}
var isUsingDefaultGetter = getOptionGroup === defaultGetOptionGroup;
var isUsingDefaultSetter = setOptionGroup === defaultSetOptionGroup;
if (isUsingDefaultGetter && !isUsingDefaultSetter || !isUsingDefaultGetter && isUsingDefaultSetter) {
console.warn("SuperSelect: Using potentially conflicting \"getOptionGroup\" and \"setOptionGroup\" implementations.\n Group reassignment after drag-and-drop operation might be broken.");
}
}, [draggable, getOptionGroup, setOptionGroup]);
var i18n = useI18nContext();
var initialValue = defaultValue !== null && defaultValue !== void 0 ? defaultValue : multiple ? [] : null;
var _React$useState = React.useState(initialValue),
_React$useState2 = _slicedToArray(_React$useState, 2),
val = _React$useState2[0],
setVal = _React$useState2[1];
var value = value_ !== undefined ? value_ : val;
/**
* Tracks whether the "Select All" feature is currently active.
* When true, new options that appear will be automatically selected.
* This state is activated when all available options are selected and
* deactivated when any option is individually deselected.
*/
var _React$useState3 = React.useState(false),
_React$useState4 = _slicedToArray(_React$useState3, 2),
isSelectAllActive = _React$useState4[0],
setIsSelectAllActive = _React$useState4[1];
/**
* Flag to prevent recursive auto-selection calls during state updates.
* Ensures that auto-selection logic doesn't trigger multiple times
* concurrently, which could cause performance issues or incorrect state.
*/
var isProcessingAutoSelectionRef = React.useRef(false);
/**
* Stores a snapshot of previously selected values to compare against
* current options for auto-selection.
*/
var previousSelectedValuesRef = React.useRef([]);
function setValue(v) {
if (!value_) {
setVal(v);
}
if (onChange) {
onChange(v);
}
}
/**
* Extracts all primitive values from an option, handling both single values and batch values.
* For batch options (arrays), returns all values flattened into a single array.
* For single options, wraps the value in an array for consistent processing.
*
* @param option - The option to extract values from
* @returns Array of primitive values contained in the option
*/
var getOptionValuesFlat = React.useCallback(function (option) {
var optionValue = getOptionValue(option);
return Array.isArray(optionValue) ? optionValue : [optionValue];
}, [getOptionValue]);
/**
* Determines if all selectable options are currently selected.
* Uses early termination to improve performance when checking if all selectable options are selected.
*
* Performance optimizations:
* - Early exit if value count is less than options count
* - Uses early termination in the validation loop
*
* @param currentValue - Current selected values array
* @param currentSelectableOptions - Array of selectable options to check
* @returns true if all selectable options are selected, false otherwise
*/
var checkIfAllSelectableOptionsSelected = React.useCallback(function (currentValue, currentSelectableOptions) {
if (!Array.isArray(currentValue) || currentSelectableOptions.length === 0) {
return false;
}
var valueSet = new Set(currentValue);
// Use early termination for better performance
var _iterator = _createForOfIteratorHelper(currentSelectableOptions),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var option = _step.value;
var optionValues = getOptionValuesFlat(option);
if (!optionValues.every(function (val) {
return valueSet.has(val);
})) {
return false;
}
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
return true;
}, [getOptionValuesFlat]);
/**
* Performs automatic selection of newly added options when Select All is active.
*
* Algorithm:
* 1. Compares current options with previous snapshot to find new options
* 2. Automatically selects any newly detected options
* 3. Implements safeguards against recursive calls
*
* Performance considerations:
* - Single pass to build current options map and detect new ones
* - Set-based deduplication for efficient value management
*
* @param currentValue - Current selected values
*/
var executeAutoSelection = React.useCallback(function (currentValue) {
// Early exit conditions for performance
if (!isSelectAllActive || !multiple || !Array.isArray(currentValue) || isProcessingAutoSelectionRef.current) {
return;
}
previousSelectedValuesRef.current = currentValue;
var newlyAddedOptions = [];
var currentValueSet = new Set(currentValue);
var _iterator2 = _createForOfIteratorHelper(selectableOptions),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var _option = _step2.value;
var _optionValues = getOptionValuesFlat(_option);
var hasNewValue = _optionValues.some(function (val) {
return !currentValueSet.has(val);
});
if (hasNewValue) {
newlyAddedOptions.push(_option);
}
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
if (newlyAddedOptions.length === 0) {
return;
}
isProcessingAutoSelectionRef.current = true;
var newValuesToAdd = [];
// Collect new values that aren't already selected
for (var _i = 0, _newlyAddedOptions = newlyAddedOptions; _i < _newlyAddedOptions.length; _i++) {
var option = _newlyAddedOptions[_i];
var optionValues = getOptionValuesFlat(option);
var _iterator3 = _createForOfIteratorHelper(optionValues),
_step3;
try {
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
var _val = _step3.value;
if (!currentValueSet.has(_val)) {
newValuesToAdd.push(_val);
}
}
} catch (err) {
_iterator3.e(err);
} finally {
_iterator3.f();
}
}
// Only update if there are new values to add
if (newValuesToAdd.length > 0) {
// Use Set for efficient deduplication
var combinedValueSet = new Set([].concat(_toConsumableArray(currentValue), newValuesToAdd));
setValue(Array.from(combinedValueSet));
}
// Reset processing flag
isProcessingAutoSelectionRef.current = false;
}, [isSelectAllActive, multiple, setValue, getOptionValuesFlat]);
// Debounced auto-selection for handling rapid option changes
var debouncedAutoSelection = React.useMemo(function () {
return debounce(executeAutoSelection, 50);
},
// Optimized 50ms debounce
[executeAutoSelection]);
var navigationList = React.useRef([]);
var virtuoso = React.useRef(null);
var searchRef = React.useRef(null);
var overlayId = useId(); // TODO use React 18 useId
var listId = useId(); // TODO use React 18 useId
var selectedValueId = useId();
var tokenListId = useId();
var _React$useState5 = React.useState(false),
_React$useState6 = _slicedToArray(_React$useState5, 2),
open = _React$useState6[0],
setOpen = _React$useState6[1];
var _React$useState7 = React.useState(false),
_React$useState8 = _slicedToArray(_React$useState7, 2),
pointer = _React$useState8[0],
setPointer = _React$useState8[1];
var _React$useState9 = React.useState(248),
_React$useState0 = _slicedToArray(_React$useState9, 2),
width = _React$useState0[0],
setWidth = _React$useState0[1];
var _React$useState1 = React.useState(248),
_React$useState10 = _slicedToArray(_React$useState1, 2),
maxHeight = _React$useState10[0],
setMaxHeight = _React$useState10[1];
var _React$useState11 = React.useState(0),
_React$useState12 = _slicedToArray(_React$useState11, 2),
listHeight = _React$useState12[0],
setListHeight = _React$useState12[1];
var _React$useState13 = React.useState(0),
_React$useState14 = _slicedToArray(_React$useState13, 2),
searchHeight = _React$useState14[0],
setSearchHeight = _React$useState14[1];
var _React$useState15 = React.useState(0),
_React$useState16 = _slicedToArray(_React$useState15, 2),
footerHeight = _React$useState16[0],
setFooterHeight = _React$useState16[1];
var listContainerHeight = Math.min(maxHeight - searchHeight - footerHeight + listContainerVerticalPadding, listHeight + listContainerVerticalPadding);
var _React$useState17 = React.useState(''),
_React$useState18 = _slicedToArray(_React$useState17, 2),
searchValue = _React$useState18[0],
setSearchValue_ = _React$useState18[1];
var setSearchValue = debounce(setSearchValue_, 250); // TODO use React 18 useDeferredValue
var _React$useState19 = React.useState(null),
_React$useState20 = _slicedToArray(_React$useState19, 2),
activeMenuIndex = _React$useState20[0],
setActiveMenuIndex = _React$useState20[1];
var tokenRemoveRefs = React.useRef([]);
var components = _objectSpread(_objectSpread({}, defaultComponents), customComponents || {});
// TODO #memogetters: consider having getOption... getter functions memoized by consumers
// Until then, exclude these callbacks from effect and memo dependencies
var sortOptions = React.useMemo(function () {
return getOptionsSortingAlgorithm({
getOptionIsBatch: getOptionIsBatch,
getOptionLabel: getOptionLabel
});
},
// skip `getOptionIsBatch` and `getOptionLabel`
[]);
var formatBatchOption = React.useMemo(function () {
return getBatchOptionFormatter({
value: value,
multiple: multiple,
getOptionIsBatch: getOptionIsBatch,
getOptionValue: getOptionValue
});
},
// skip `getOptionIsBatch` and `getOptionValue`, refer to TODO #memogetters
[value, multiple]);
// collect groups, sort them and populate them with options, and sort the options inside the groups
var enforceOptionsSortingOrder = React.useCallback(function groupAndSort(opts) {
var _collectGroupsInOrder = collectGroupsInOrderOfOccurrence(opts, getOptionGroup),
groups = _collectGroupsInOrder.groups,
groupedOptions = _collectGroupsInOrder.groupedOptions;
if (getIsAllOptionsUngrouped(groups)) {
return opts.sort(sortOptions);
}
return Object.entries(groupedOptions).sort(function (_ref4, _ref5) {
var _ref6 = _slicedToArray(_ref4, 1),
groupA = _ref6[0];
var _ref7 = _slicedToArray(_ref5, 1),
groupB = _ref7[0];
return sortOptgroups(groupA, groupB);
}).flatMap(function (_ref8) {
var _ref9 = _slicedToArray(_ref8, 2),
groupName = _ref9[0],
groupOptions = _ref9[1];
return createOptgroup(groupName, groupOptions.sort(sortOptions));
});
},
// skip `getOptionGroup`, refer to TODO #memogetters
[sortOptions]);
// collect groups and populate them with options
var deriveOptionsSortingOrder = React.useCallback(function (opts) {
var _collectGroupsInOrder2 = collectGroupsInOrderOfOccurrence(opts, getOptionGroup),
groups = _collectGroupsInOrder2.groups,
groupedOptions = _collectGroupsInOrder2.groupedOptions;
if (getIsAllOptionsUngrouped(groups)) {
return opts;
}
// display optgroups in the order of occurrence, as they are considered pre-sorted
return groups.flatMap(function (group) {
var groupOptions = groupedOptions[group];
return createOptgroup(group, groupOptions);
});
},
// skip `getOptionGroup`, refer to TODO #memogetters
[]);
var groupAndSortAlgorithm = React.useMemo(function () {
return sort ? enforceOptionsSortingOrder : deriveOptionsSortingOrder;
}, [sort, enforceOptionsSortingOrder, deriveOptionsSortingOrder]);
var options = React.useMemo(function () {
var queried = searchValue ? sourceOptions.filter(function (opt) {
return stringContains(getOptionLabel(opt), searchValue);
}) : _toConsumableArray(sourceOptions); // make a copy of source options to prevent the mutation when sorting
return groupAndSortAlgorithm(queried).map(formatBatchOption);
},
// skip `getOptionLabel`, refer to TODO #memogetters
[groupAndSortAlgorithm, formatBatchOption, searchValue, sourceOptions]);
/**
* Filtered list of options that can be selected (excludes disabled options and optgroups).
* Used for auto-selection logic to determine which options should be automatically
* selected when Select All is active and new options appear.
*/
var selectableOptions = React.useMemo(function () {
var result = [];
var _iterator4 = _createForOfIteratorHelper(options),
_step4;
try {
for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
var option = _step4.value;
if (isSelectableOption(option)) {
result.push(option);
}
}
} catch (err) {
_iterator4.e(err);
} finally {
_iterator4.f();
}
return result;
}, [options]);
/**
* Effect function that triggers auto-selection when conditions are met.
* Calls the debounced auto-selection logic when:
* - Select All is enabled
* - Multiple selection mode is active
* - Select All state is currently active
* - Current value is an array (multiple selection)
*
* The debouncing helps handle rapid option changes efficiently.
*/
var autoSelectionEffect = React.useCallback(function () {
if (selectAllEnabled && multiple && isSelectAllActive && Array.isArray(value) && value.length) {
debouncedAutoSelection(value);
}
}, [selectAllEnabled, multiple, isSelectAllActive, value, debouncedAutoSelection]);
/**
* Effect that manages auto-selection behavior and cleanup.
* - Triggers auto-selection when dependencies change
* - Runs on every render when auto-selection conditions might have changed
*/
React.useEffect(function () {
autoSelectionEffect();
return function () {
var _debouncedAutoSelecti;
// Cleanup debounced function
(_debouncedAutoSelecti = debouncedAutoSelection.cancel) === null || _debouncedAutoSelecti === void 0 ? void 0 : _debouncedAutoSelecti.call(debouncedAutoSelection);
// Reset processing flag
isProcessingAutoSelectionRef.current = false;
};
}, [autoSelectionEffect, debouncedAutoSelection]);
var selectAllOption = _defineProperty({
label: i18n.t('core.select.selectAll'),
id: 'selectAll'
}, isOptSelectAllSymbol, true);
var allOptionsWithSelectAll = React.useMemo(function () {
if (!selectAllEnabled || !multiple) {
return options;
}
return [selectAllOption].concat(_toConsumableArray(options));
}, [selectAllEnabled, multiple, selectAllOption, options]);
var _React$useState21 = React.useState([]),
_React$useState22 = _slicedToArray(_React$useState21, 2),
draggableOptions = _React$useState22[0],
setDraggableOptions = _React$useState22[1];
React.useEffect(function () {
if (!draggable) {
return;
}
// make a copy of source options to prevent the mutation when sorting
var opts = groupAndSortAlgorithm(sourceOptions.map(function (opt) {
return _objectSpread(_objectSpread({}, opt), {}, _defineProperty({}, draggableOptionIdSymbol, ulid()));
}));
setDraggableOptions(opts);
},
// in draggable mode, sourceOptions must be memoized, othewise reorder will not work
[draggable, sourceOptions, groupAndSortAlgorithm]);
var queriedDraggableOptions = React.useMemo(function () {
if (!draggable) {
return [];
}
var queried = searchValue ? removeEmptyOptGroups(draggableOptions.filter(function (opt) {
if (!getOptionIsOptgroup(opt)) {
return stringContains(getOptionLabel(opt), searchValue);
}
return true;
})) : draggableOptions;
return queried.map(formatBatchOption);
},
// skip `getOptionLabel`, refer to TODO #memogetters
[draggable, draggableOptions, searchValue, formatBatchOption]);
// Group indices calculation - needs allOptionsWithSelectAll because:
// When SelectAll is enabled, it becomes the first item (index 0) and shifts all other indices by 1.
// Group headers need their correct absolute positions in the combined list for navigation to work.
var groupIndices = React.useMemo(function () {
var baseOptions = selectAllEnabled && multiple ? allOptionsWithSelectAll : options;
return baseOptions.reduce(function (acc, opt, i) {
if (getOptionIsOptgroup(opt)) {
acc.push(i);
}
return acc;
}, []);
}, [selectAllEnabled, multiple, allOptionsWithSelectAll, options]);
// Selected index calculation - needs allOptionsWithSelectAll because:
// The selectedIndex must reference the absolute position in the combined list for keyboard navigation.
// When SelectAll is at index 0, the first regular option becomes index 1, affecting selection highlighting.
var selectedIndex = React.useMemo(function () {
var baseOptions = selectAllEnabled && multiple ? allOptionsWithSelectAll : options;
return baseOptions.findIndex(function (option) {
return isSelectableOption(option) && getOptionValue(option) === value;
});
}, [selectAllEnabled, multiple, allOptionsWithSelectAll, options, value]);
// Selected option lookup - needs allOptionsWithSelectAll because:
// The selectedOption lookup must search the complete list including SelectAll to find matches.
// This ensures proper option detection regardless of whether SelectAll or regular options are selected.
var selectedOption = React.useMemo(function () {
var val = Array.isArray(value) ? value[0] : value;
var baseOptions = selectAllEnabled && multiple ? allOptionsWithSelectAll : options;
return baseOptions.find(function (option) {
return getOptionValue(option) === val;
});
}, [selectAllEnabled, multiple, allOptionsWithSelectAll, options, value]);
var selectedLabel = React.useMemo(function () {
return selectedOption ? getOptionLabel(selectedOption) : '';
}, [selectedOption]);
function isSelectableOption(o) {
return !getOptionIsOptgroup(o) && !getOptionIsDisabled(o);
}
function getFirstSelectableOptionIndex() {
// When Select All is enabled in multiple mode, it's always at index 0 and is selectable
if (selectAllEnabled && multiple) {
return 0;
}
var baseOptions = options;
return baseOptions.findIndex(isSelectableOption) + (selectAllEnabled && multiple ? 1 : 0);
}
// Helper functions for onSelect optimization
/**
* Handles the toggle behavior for the Select All option.
*
* Behavior:
* - If all options are selected: deselects all and deactivates Select All
* - If not all options are selected: selects all and activates Select All
*
* Performance optimizations:
* - Uses Set for efficient value deduplication
* - Leverages existing option checking logic
* - Calls optional onSelectAll callback for external handling
*/
var handleSelectAllToggle = React.useCallback(function () {
var getIsSelected = getOptionIsSelected !== null && getOptionIsSelected !== void 0 ? getOptionIsSelected : defaultGetOptionIsSelected;
var allSelected = selectableOptions.every(function (opt) {
return getIsSelected(opt);
});
if (allSelected) {
var newValue = onUnselectAll(value, options, searchValue, getOptionValuesFlat);
setValue(newValue);
} else {
if (onSelectAll) {
onSelectAll(selectableOptions);
}
var currentValue = Array.isArray(value) ? value : [];
var valueSet = new Set(currentValue);
selectableOptions.forEach(function (opt) {
getOptionValuesFlat(opt).forEach(function (val) {
return valueSet.add(val);
});
});
setValue(Array.from(valueSet));
}
}, [getOptionIsSelected, selectableOptions, onSelectAll, setIsSelectAllActive, setValue, value, getOptionValuesFlat]);
/**
* Determines if Select All should be activated based on the new value.
* Select All activates when all selectable options become selected,
* but only if it wasn't already active (prevents unnecessary state changes).
*
* @param currentSelectableOptions - Current array of selectable options to check against
* @returns true if Select All should be activated, false otherwise
*/
var shouldActivateSelectAll = React.useCallback(function (currentSelectableOptions) {
return selectAllEnabled && !searchValue && checkIfAllSelectableOptionsSelected(value, currentSelectableOptions);
}, [selectAllEnabled, searchValue, value, checkIfAllSelectableOptionsSelected]);
// Centralize select all active state
React.useEffect(function () {
var shouldBeActive = shouldActivateSelectAll(selectableOptions) && selectableOptions.length && !!value.length;
// When Select All is already active and new selectable options are added,
// skip updating the Select All active state here. This allows the existing
// auto-selection logic to handle newly added options without triggering
// unnecessary state changes in this effect.
var hasNewOptions = selectableOptions.length > previousSelectedValuesRef.current.length;
var hasRemovedValue = (value || []).length < previousSelectedValuesRef.current.length;
if (isSelectAllActive && hasNewOptions && !hasRemovedValue && value.length) {
return;
}
if (shouldBeActive) {
setIsSelectAllActive(true);
} else {
setIsSelectAllActive(false);
}
}, [isSelectAllActive, selectableOptions, shouldActivateSelectAll, value]);
/**
* Handles selection of batch options (options with multiple values).
* Adds all values from the batch option to the current selection
* and activates Select All if all options become selected.
*
* @param optionValue - Array of primitive values from the batch option
*/
var handleBatchOptionSelection = React.useCallback(function (optionValue) {
// Ensure we're working with an array for multiple selection
var currentValue = Array.isArray(value) ? value : [];
var newValueSet = new Set([].concat(_toConsumableArray(currentValue), _toConsumableArray(optionValue)));
var newValue = Array.from(newValueSet);
setValue(newValue);
}, [value, setValue]);
/**
* Handles selection/deselection of individual options in multiple mode.
*
* Behavior:
* - If option is selected: removes it and deactivates Select All
* - If option is not selected: adds it and activates Select All if all options become selected
*
* @param optionValue - The primitive value of the option to toggle
*/
var handleSingleOptionSelection = React.useCallback(function (optionValue) {
// Ensure we're working with an array for multiple selection
if (!Array.isArray(value)) {
return;
}
var valueIndex = value.indexOf(optionValue);
if (valueIndex >= 0) {
// Remove value
var newValue = value.filter(function (_, i) {
return i !== valueIndex;
});
setValue(newValue);
} else {
// Add value
var _newValue = [].concat(_toConsumableArray(value), [optionValue]);
setValue(_newValue);
}
}, [value, setValue]);
var onSelect = React.useCallback(function (option) {
if (getOptionIsDisabled(option)) {
return;
}
var optionValue = getOptionValue(option);
var isSelectAll = getOptionIsOptSelectAll(option);
if (isMultiple(multiple, value)) {
if (isSelectAll) {
handleSelectAllToggle();
} else if (Array.isArray(optionValue)) {
handleBatchOptionSelection(optionValue);
} else {
handleSingleOptionSelection(optionValue);
}
} else {
setValue(optionValue);
}
}, [getOptionIsDisabled, getOptionValue, getOptionIsOptSelectAll, multiple, value, handleSelectAllToggle, handleBatchOptionSelection, handleSingleOptionSelection, setValue]);
function onKeyboardSelect() {
if (activeMenuIndex !== null) {
// Select All must be included in the list for keyboard navigation to work properly.
// The activeMenuIndex corresponds to positions in the complete navigation list including SelectAll.
var baseOptions = selectAllEnabled && multiple ? allOptionsWithSelectAll : options;
var option = baseOptions[activeMenuIndex];
if (option) {
onSelect(option);
}
}
}
function defaultGetOptionIsSelected(option) {
if (Array.isArray(value)) {
var optionValues = getOptionValue(option);
if (Array.isArray(optionValues)) {
return optionValues.every(function (v) {
return value.includes(v);
});
}
return value.includes(optionValues);
}
return value === getOptionValue(option);
}
function defaultGetOptionIsPartiallySelected(option) {
if (selectionStyle !== 'checkbox') {
return false;
}
if (Array.isArray(value)) {
var optionValues = getOptionValue(option);
if (Array.isArray(optionValues)) {
return optionValues.some(function (v) {
return value.includes(v);
}) && !optionValues.every(function (v) {
return value.includes(v);
});
}
}
return false;
}
function defaultGetOptionIsSelectAllPartiallySelected(options, isSelected, isPartiallySelected) {
if (selectionStyle !== 'checkbox' && !Array.isArray(value)) {
return false;
}
return options.some(function (option) {
return isSelected(option) || isPartiallySelected(option);
}) && !options.every(function (option) {
return isSelected(option);
});
}
function defaultGetOptionIsSelectAllSelected(options, isSelected) {
if (!multiple) {
return false;
}
return options.every(function (option) {
return isSelected(option);
});
}
function isEmpty() {
return Array.isArray(value) ? value.length === 0 : value === null;
}
var boundWidthRef = React.useRef(open);
React.useEffect(function () {
boundWidthRef.current = open;
}, [open]);
var floating = useFloating({
open: open,
onOpenChange: function onOpenChange(nextOpen) {
setOpen(nextOpen);
_onOpenChange(nextOpen);
if (!nextOpen && multiple) {
requestAnimationFrame(function () {
var _searchRef$current;
(_searchRef$current = searchRef.current) === null || _searchRef$current === void 0 ? void 0 : _searchRef$current.focus();
});
}
},
whileElementsMounted: function whileElementsMounted() {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return (/* { animationFrame: true } is used to fix ResizeObserver issue with @floating-ui/react */
autoUpdate.apply(void 0, args.concat([{
animationFrame: true
}]))
);
},
strategy: 'fixed',
middleware: [offset(function (_ref0) {
var rects = _ref0.rects;
return {
mainAxis: 2,
crossAxis: overlayMatchesTriggerWidth ? 0 : (rects.floating.width - rects.reference.width) / 2
};
}), flip(), size({
apply: function apply(_ref1) {
var availableHeight = _ref1.availableHeight,
elements = _ref1.elements;
if (boundWidthRef.current) {
boundWidthRef.current = false;
setWidth(overlayMatchesTriggerWidth ? elements.reference.getBoundingClientRect().width : extendedSelectMenuWidth);
}
if (availableHeight >= reasonableDropdownMinimumHeight) {
setMaxHeight(availableHeight);
}
},
padding: 10
})]
});
var _useZIndexContext = useZIndexContext(),
zIndex = _useZIndexContext.value;
var activeDescendantId = React.useMemo(function () {
if (activeMenuIndex === null) return undefined;
var baseOptions = selectAllEnabled && multiple ? allOptionsWithSelectAll : options;
var option = baseOptions[activeMenuIndex];
if (!option) return undefined;
if (getOptionIsOptSelectAll(option)) return "item-".concat(selectAllOption.id);
return "item-".concat(getOptionValue(option));
}, [activeMenuIndex, selectAllEnabled, multiple, allOptionsWithSelectAll, options, selectAllOption]);
function getAriaLabelProps() {
return _objectSpread(_objectSpread({}, ariaLabel && {
'aria-label': ariaLabel
}), ariaLabelledBy && {
'aria-labelledby': ariaLabelledBy
});
}
var _useInteractions = useInteractions([useClick(floating.context, {
enabled: !disabled,
keyboardHandlers: false
}), useDismiss(floating.context, {
enabled: !disabled
}), useListNavigation(floating.context, {
activeIndex: activeMenuIndex,
disabledIndices: groupIndices,
enabled: !disabled,
listRef: navigationList,
loop: true,
onNavigate: function onNavigate(i) {
return setActiveMenuIndex(function () {
return i;
});
},
selectedIndex: selectedIndex,
virtual: true
}), useTokenNavigation(floating.context, {
enabled: !disabled && multiple && searchValue === '',
tokenRemoveRefs: tokenRemoveRefs,
value: value
}), useKeyboardSelection(floating.context, {
enabled: !disabled,
onSelect: onKeyboardSelect
}), {
reference: _objectSpread(_objectSpread({
$block: block,
$disabled: disabled,
$error: error,
$hasClearIcon: !disabled && !loading,
$loading: loading,
$multiple: multiple,
$open: open,
$placeholder: !selectedLabel,
'aria-activedescendant': multiple ? undefined : activeDescendantId
}, !multiple && _objectSpread(_objectSpread({
'aria-controls': open ? overlayId : undefined,
'aria-describedby': selectedValueId,
'aria-expanded': open,
'aria-haspopup': 'listbox'
}, getAriaLabelProps()), {}, {
role: 'button',
tabIndex: disabled ? -1 : tabIndex
})), {}, {
onKeyDown: function onKeyDown() {
setPointer(false);
},
search: search
}),
// TODO fix type
floating: {
role: search ? 'listbox' : 'none',
id: overlayId,
onKeyDown: function onKeyDown() {
setPointer(false);
},
onPointerMove: function onPointerMove() {
setPointer(true);
},
style: {
position: floating.strategy,
left: floating.x || 0,
top: floating.y || 0,
width: width,
maxHeight: maxHeight,
zIndex: zIndex
}
}
}]),
getReferenceProps = _useInteractions.getReferenceProps,
getFloatingProps = _useInteractions.getFloatingProps,
getItemProps = _useInteractions.getItemProps;
function getLabelProps() {
return {
$hoverable: false,
id: selectedValueId
};
}
function getMultiInputProps() {
var showPlaceholder = isEmpty();
return _objectSpread(_objectSpread({
'aria-activedescendant': activeDescendantId,
'aria-controls': open ? overlayId : undefined,
'aria-expanded': open,
'aria-haspopup': 'listbox'
}, getAriaLabelProps()), {}, {
role: 'combobox',
ref: searchRef,
tabIndex: disabled ? -1 : tabIndex,
placeholder: showPlaceholder ? placeholder : '',
disabled: disabled,
onKeyDown: function onKeyDown(e) {
if (e.key === 'Tab') {
if (open) {
var _floating$refs$floati;
e.preventDefault();
e.stopPropagation();
(_floating$refs$floati = floating.refs.floating.current) === null || _floating$refs$floati === void 0 ? void 0 : _floating$refs$floati.focus();
}
}
}
});
}
function getMultiValueProps(index) {
function removeToken() {
if (!isMultiple(multiple, value)) return;
var nextVal = value.filter(function (_, i) {
return i !== index;
});
setValue(nextVal);
requestAnimationFrame(function () {
if (nextVal.length === 0) {
var _searchRef$current2;
(_searchRef$current2 = searchRef.current) === null || _searchRef$current2 === void 0 ? void 0 : _searchRef$current2.focus();
} else if (index >= nextVal.length) {
var _tokenRemoveRefs$curr2;
(_tokenRemoveRefs$curr2 = tokenRemoveRefs.current[nextVal.length - 1]) === null || _tokenRemoveRefs$curr2 === void 0 ? void 0 : _tokenRemoveRefs$curr2.focus();
} else {
var _tokenRemoveRefs$curr3;
(_tokenRemoveRefs$curr3 = tokenRemoveRefs.current[index]) === null || _tokenRemoveRefs$curr3 === void 0 ? void 0 : _tokenRemoveRefs$curr3.focus();
}
});
}
return {
ref: function ref(el) {
tokenRemoveRefs.current[index] = el;
},
onClick: function onClick(e) {
e.stopPropagation();
removeToken();
},
onKeyDown: function onKeyDown(e) {
if (e.key === 'Enter' || e.key === 'Backspace' || e.key === 'Delete') {
e.preventDefault();
e.stopPropagation();
removeToken();
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
e.stopPropagation();
if (index > 0) {
var _tokenRemoveRefs$curr4;
(_tokenRemoveRefs$curr4 = tokenRemoveRefs.current[index - 1]) === null || _tokenRemoveRefs$curr4 === void 0 ? void 0 : _tokenRemoveRefs$curr4.focus();
}
} else if (e.key === 'ArrowRight') {
e.preventDefault();
e.stopPropagation();
if (isMultiple(multiple, value) && index < value.length - 1) {
var _tokenRemoveRefs$curr5;
(_tokenRemoveRefs$curr5 = tokenRemoveRefs.current[index + 1]) === null || _tokenRemoveRefs$curr5 === void 0 ? void 0 : _tokenRemoveRefs$curr5.focus();
} else {
var _searchRef$current3;
(_searchRef$current3 = searchRef.current) === null || _searchRef$current3 === void 0 ? void 0 : _searchRef$current3.focus();
}
}
}
};
}
function getSearchContainerProps() {
return {
ref: function ref(el) {
var _el$clientHeight;
setSearchHeight((_el$clientHeight = el === null || el === void 0 ? void 0 : el.clientHeight) !== null && _el$clientHeight !== void 0 ? _el$clientHeight : 0);
}
};
}
function getSearchProps() {
return {
'aria-activedescendant': activeDescendantId,
'aria-controls': listId,
'aria-expanded': open,
'aria-haspopup': 'listbox',
onChange: function onChange(value) {
setSearchValue(value);
},
placeholder: i18n.t('core.select.search'),
role: 'combobox'
};
}
function getHeaderProps() {
return {
ref: function ref(el) {}
};
}
function getFooterProps() {
return {
ref: function ref(el) {
var _el$clientHeight2;
setFooterHeight((_el$clientHeight2 = el === null || el === void 0 ? void 0 : el.clientHeight) !== null && _el$clientHeight2 !== void 0 ? _el$clientHeight2 : 0);
},
onKeyDown: function onKeyDown(e) {
if (e.key !== 'Escape') {
e.stopPropagation();
}
}
};
}
function getEmptyMessageProps() {
return {
emptyMessage: emptyMessage
};
}
function getClearProps() {
return {
'aria-hidden': true,
'aria-label': i18n.t('core.select.clear'),
tabIndex: -1,
onClick: function onClick(e) {
// prevent the menu from closing
e.stopPropagation();
setValue(multiple ? [] : null);
setOpen(true);
}
};
}
function getMenuProps() {
return {
scrollable: false,
style: {
height: listContainerHeight
}
};
}
function getVirtuosoProps() {
var atBottomStateChange = function atBottomStateChange(atBottom) {
if (atBottom) {
onScrollBottom();
}
};
return {
role: 'listbox',
id: listId,
data: draggable ? queriedDraggableOptions : options,
components: {
Item: components === null || components === void 0 ? void 0 : components.Item // TODO fix type
},
initialTopMostItemIndex: selectedIndex >= 0 ? selectedIndex : 0,
style: {
flex: '1 1 auto'
},
tabIndex: -1,
totalListHeightChanged: setListHeight,
atBottomStateChange: atBottomStateChange
};
}
var onDragEnd = React.useCallback(function (result) {
if (!result.destination) {
return;
}
if (result.source.index === result.destination.index) {
return;
}
var startIndex = result.source.index;
var destinationIndex = result.destination.index;
setDraggableOptions(function (options) {
var _reorder = reorder(options, startIndex, destinationIndex),
nextOptions = _reorder.nextOptions,
prevGroup = _reorder.prevGroup,
nextGroup = _reorder.nextGroup;
var adjustedOptions = prevGroup !== nextGroup ? nextOptions.map(function (option, index) {
if (index === destinationIndex) {
return setOptionGroup(option, nextGroup);
}
return option;
}) : nextOptions;
if (typeof onManualSort === 'function') {
setTimeout(function () {
onManualSort(adjustedOptions.filter(function (option) {
return !getOptionIsOptgroup(option);
}));
}, 0);
}
return adjustedOptions;
});
}, []);
// run effect when the `open` state changes
//
// if it was closed and is now open, reset the highlighted index to be
// the first selectable option
//
// if it was open and is now closed, reset the search value and focused token index
React.useEffect(function () {
if (open) {
setActiveMenuIndex(function () {
return selectedIndex >= 0 ? selectedIndex : getFirstSelecta