@mantine/core
Version:
React components library focused on usability, accessibility and developer experience
659 lines (656 loc) • 19.4 kB
JavaScript
import React, { forwardRef, useRef, useState } from 'react';
import { useId, useUncontrolled, useScrollIntoView, useDidUpdate, useMergedRef } from '@mantine/hooks';
import { getDefaultZIndex, useComponentDefaultProps } from '@mantine/styles';
import { groupOptions } from '@mantine/utils';
import { DefaultValue } from './DefaultValue/DefaultValue.js';
import { DefaultItem } from '../Select/DefaultItem/DefaultItem.js';
import { filterData } from './filter-data/filter-data.js';
import { getSelectRightSectionProps } from '../Select/SelectRightSection/get-select-right-section-props.js';
import { SelectScrollArea } from '../Select/SelectScrollArea/SelectScrollArea.js';
import { SelectPopover } from '../Select/SelectPopover/SelectPopover.js';
import { SelectItems } from '../Select/SelectItems/SelectItems.js';
import useStyles from './MultiSelect.styles.js';
import { extractSystemStyles } from '../Box/style-system-props/extract-system-styles/extract-system-styles.js';
import { Input } from '../Input/Input.js';
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
var __objRest = (source, exclude) => {
var target = {};
for (var prop in source)
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
target[prop] = source[prop];
if (source != null && __getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(source)) {
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
target[prop] = source[prop];
}
return target;
};
function defaultFilter(value, selected, item) {
if (selected) {
return false;
}
return item.label.toLowerCase().trim().includes(value.toLowerCase().trim());
}
function defaultShouldCreate(query, data) {
return !!query && !data.some((item) => item.value.toLowerCase() === query.toLowerCase());
}
function filterValue(value, data) {
if (!Array.isArray(value)) {
return void 0;
}
if (data.length === 0) {
return [];
}
const flatData = data.map((item) => {
if (typeof item === "object") {
return item.value;
}
return item;
});
return value.filter((val) => flatData.includes(val));
}
const defaultProps = {
size: "sm",
valueComponent: DefaultValue,
itemComponent: DefaultItem,
transitionProps: { transition: "fade", duration: 0 },
maxDropdownHeight: 220,
shadow: "sm",
searchable: false,
filter: defaultFilter,
limit: Infinity,
clearSearchOnChange: true,
clearable: false,
clearSearchOnBlur: false,
disabled: false,
initiallyOpened: false,
creatable: false,
shouldCreate: defaultShouldCreate,
switchDirectionOnFlip: false,
zIndex: getDefaultZIndex("popover"),
selectOnBlur: false,
positionDependencies: [],
dropdownPosition: "flip"
};
const MultiSelect = forwardRef((props, ref) => {
const _a = useComponentDefaultProps("MultiSelect", defaultProps, props), {
className,
style,
required,
label,
description,
size,
error,
classNames,
styles,
wrapperProps,
value,
defaultValue,
data,
onChange,
valueComponent: Value,
itemComponent,
id,
transitionProps,
maxDropdownHeight,
shadow,
nothingFound,
onFocus,
onBlur,
searchable,
placeholder,
filter,
limit,
clearSearchOnChange,
clearable,
clearSearchOnBlur,
variant,
onSearchChange,
searchValue,
disabled,
initiallyOpened,
radius,
icon,
rightSection,
rightSectionWidth,
creatable,
getCreateLabel,
shouldCreate,
onCreate,
sx,
dropdownComponent,
onDropdownClose,
onDropdownOpen,
maxSelectedValues,
withinPortal,
switchDirectionOnFlip,
zIndex,
selectOnBlur,
name,
dropdownPosition,
errorProps,
labelProps,
descriptionProps,
form,
positionDependencies,
onKeyDown,
unstyled,
inputContainer,
inputWrapperOrder,
readOnly,
withAsterisk,
clearButtonProps,
hoverOnSearchChange,
disableSelectedItemFiltering
} = _a, others = __objRest(_a, [
"className",
"style",
"required",
"label",
"description",
"size",
"error",
"classNames",
"styles",
"wrapperProps",
"value",
"defaultValue",
"data",
"onChange",
"valueComponent",
"itemComponent",
"id",
"transitionProps",
"maxDropdownHeight",
"shadow",
"nothingFound",
"onFocus",
"onBlur",
"searchable",
"placeholder",
"filter",
"limit",
"clearSearchOnChange",
"clearable",
"clearSearchOnBlur",
"variant",
"onSearchChange",
"searchValue",
"disabled",
"initiallyOpened",
"radius",
"icon",
"rightSection",
"rightSectionWidth",
"creatable",
"getCreateLabel",
"shouldCreate",
"onCreate",
"sx",
"dropdownComponent",
"onDropdownClose",
"onDropdownOpen",
"maxSelectedValues",
"withinPortal",
"switchDirectionOnFlip",
"zIndex",
"selectOnBlur",
"name",
"dropdownPosition",
"errorProps",
"labelProps",
"descriptionProps",
"form",
"positionDependencies",
"onKeyDown",
"unstyled",
"inputContainer",
"inputWrapperOrder",
"readOnly",
"withAsterisk",
"clearButtonProps",
"hoverOnSearchChange",
"disableSelectedItemFiltering"
]);
const { classes, cx, theme } = useStyles({ invalid: !!error }, { name: "MultiSelect", classNames, styles, unstyled, size, variant });
const { systemStyles, rest } = extractSystemStyles(others);
const inputRef = useRef();
const itemsRefs = useRef({});
const uuid = useId(id);
const [dropdownOpened, setDropdownOpened] = useState(initiallyOpened);
const [hovered, setHovered] = useState(-1);
const [direction, setDirection] = useState("column");
const [_searchValue, handleSearchChange] = useUncontrolled({
value: searchValue,
defaultValue: "",
finalValue: void 0,
onChange: onSearchChange
});
const [IMEOpen, setIMEOpen] = useState(false);
const { scrollIntoView, targetRef, scrollableRef } = useScrollIntoView({
duration: 0,
offset: 5,
cancelable: false,
isList: true
});
const isCreatable = creatable && typeof getCreateLabel === "function";
let createLabel = null;
const formattedData = data.map((item) => typeof item === "string" ? { label: item, value: item } : item);
const sortedData = groupOptions({ data: formattedData });
const [_value, setValue] = useUncontrolled({
value: filterValue(value, data),
defaultValue: filterValue(defaultValue, data),
finalValue: [],
onChange
});
const valuesOverflow = useRef(!!maxSelectedValues && maxSelectedValues < _value.length);
const handleValueRemove = (_val) => {
if (!readOnly) {
const newValue = _value.filter((val) => val !== _val);
setValue(newValue);
if (!!maxSelectedValues && newValue.length < maxSelectedValues) {
valuesOverflow.current = false;
}
}
};
const handleInputChange = (event) => {
handleSearchChange(event.currentTarget.value);
!disabled && !valuesOverflow.current && searchable && setDropdownOpened(true);
};
const handleInputFocus = (event) => {
typeof onFocus === "function" && onFocus(event);
!disabled && !valuesOverflow.current && searchable && setDropdownOpened(true);
};
const filteredData = filterData({
data: sortedData,
searchable,
searchValue: _searchValue,
limit,
filter,
value: _value,
disableSelectedItemFiltering
});
const getNextIndex = (index, nextItem, compareFn) => {
let i = index;
while (compareFn(i)) {
i = nextItem(i);
if (!filteredData[i].disabled)
return i;
}
return index;
};
useDidUpdate(() => {
if (hoverOnSearchChange && _searchValue) {
setHovered(0);
} else {
setHovered(-1);
}
}, [_searchValue, hoverOnSearchChange]);
useDidUpdate(() => {
if (!disabled && _value.length > data.length) {
setDropdownOpened(false);
}
if (!!maxSelectedValues && _value.length < maxSelectedValues) {
valuesOverflow.current = false;
}
if (!!maxSelectedValues && _value.length >= maxSelectedValues) {
valuesOverflow.current = true;
setDropdownOpened(false);
}
}, [_value]);
const handleItemSelect = (item) => {
if (!readOnly) {
clearSearchOnChange && handleSearchChange("");
if (_value.includes(item.value)) {
handleValueRemove(item.value);
} else {
if (item.creatable && typeof onCreate === "function") {
const createdItem = onCreate(item.value);
if (typeof createdItem !== "undefined" && createdItem !== null) {
if (typeof createdItem === "string") {
setValue([..._value, createdItem]);
} else {
setValue([..._value, createdItem.value]);
}
}
} else {
setValue([..._value, item.value]);
}
if (_value.length === maxSelectedValues - 1) {
valuesOverflow.current = true;
setDropdownOpened(false);
}
if (hovered === filteredData.length - 1) {
setHovered(filteredData.length - 2);
}
if (filteredData.length === 1) {
setDropdownOpened(false);
}
}
}
};
const handleInputBlur = (event) => {
typeof onBlur === "function" && onBlur(event);
if (selectOnBlur && filteredData[hovered] && dropdownOpened) {
handleItemSelect(filteredData[hovered]);
}
clearSearchOnBlur && handleSearchChange("");
setDropdownOpened(false);
};
const handleInputKeydown = (event) => {
if (IMEOpen) {
return;
}
onKeyDown == null ? void 0 : onKeyDown(event);
if (readOnly) {
return;
}
if (event.key !== "Backspace" && !!maxSelectedValues && valuesOverflow.current) {
return;
}
const isColumn = direction === "column";
const handleNext = () => {
setHovered((current) => {
var _a2;
const nextIndex = getNextIndex(current, (index) => index + 1, (index) => index < filteredData.length - 1);
if (dropdownOpened) {
targetRef.current = itemsRefs.current[(_a2 = filteredData[nextIndex]) == null ? void 0 : _a2.value];
scrollIntoView({
alignment: isColumn ? "end" : "start"
});
}
return nextIndex;
});
};
const handlePrevious = () => {
setHovered((current) => {
var _a2;
const nextIndex = getNextIndex(current, (index) => index - 1, (index) => index > 0);
if (dropdownOpened) {
targetRef.current = itemsRefs.current[(_a2 = filteredData[nextIndex]) == null ? void 0 : _a2.value];
scrollIntoView({
alignment: isColumn ? "start" : "end"
});
}
return nextIndex;
});
};
switch (event.key) {
case "ArrowUp": {
event.preventDefault();
setDropdownOpened(true);
isColumn ? handlePrevious() : handleNext();
break;
}
case "ArrowDown": {
event.preventDefault();
setDropdownOpened(true);
isColumn ? handleNext() : handlePrevious();
break;
}
case "Enter": {
event.preventDefault();
if (filteredData[hovered] && dropdownOpened) {
handleItemSelect(filteredData[hovered]);
} else {
setDropdownOpened(true);
}
break;
}
case " ": {
if (!searchable) {
event.preventDefault();
if (filteredData[hovered] && dropdownOpened) {
handleItemSelect(filteredData[hovered]);
} else {
setDropdownOpened(true);
}
}
break;
}
case "Backspace": {
if (_value.length > 0 && _searchValue.length === 0) {
setValue(_value.slice(0, -1));
setDropdownOpened(true);
if (maxSelectedValues) {
valuesOverflow.current = false;
}
}
break;
}
case "Home": {
if (!searchable) {
event.preventDefault();
if (!dropdownOpened) {
setDropdownOpened(true);
}
const firstItemIndex = filteredData.findIndex((item) => !item.disabled);
setHovered(firstItemIndex);
scrollIntoView({
alignment: isColumn ? "end" : "start"
});
}
break;
}
case "End": {
if (!searchable) {
event.preventDefault();
if (!dropdownOpened) {
setDropdownOpened(true);
}
const lastItemIndex = filteredData.map((item) => !!item.disabled).lastIndexOf(false);
setHovered(lastItemIndex);
scrollIntoView({
alignment: isColumn ? "end" : "start"
});
}
break;
}
case "Escape": {
setDropdownOpened(false);
}
}
};
const selectedItems = _value.map((val) => {
let selectedItem = sortedData.find((item) => item.value === val && !item.disabled);
if (!selectedItem && isCreatable) {
selectedItem = {
value: val,
label: val
};
}
return selectedItem;
}).filter((val) => !!val).map((item) => /* @__PURE__ */ React.createElement(Value, __spreadProps(__spreadValues({}, item), {
variant,
disabled,
className: classes.value,
readOnly,
onRemove: (event) => {
event.preventDefault();
event.stopPropagation();
handleValueRemove(item.value);
},
key: item.value,
size,
styles,
classNames,
radius
})));
const isItemSelected = (itemValue) => _value.includes(itemValue);
const handleClear = () => {
var _a2;
handleSearchChange("");
setValue([]);
(_a2 = inputRef.current) == null ? void 0 : _a2.focus();
if (maxSelectedValues) {
valuesOverflow.current = false;
}
};
if (isCreatable && shouldCreate(_searchValue, sortedData)) {
createLabel = getCreateLabel(_searchValue);
filteredData.push({ label: _searchValue, value: _searchValue, creatable: true });
}
const shouldRenderDropdown = !readOnly && (filteredData.length > 0 ? dropdownOpened : dropdownOpened && !!nothingFound);
useDidUpdate(() => {
const handler = shouldRenderDropdown ? onDropdownOpen : onDropdownClose;
typeof handler === "function" && handler();
}, [shouldRenderDropdown]);
return /* @__PURE__ */ React.createElement(Input.Wrapper, __spreadValues(__spreadValues({
required,
id: uuid,
label,
error,
description,
size,
className,
style,
classNames,
styles,
__staticSelector: "MultiSelect",
sx,
errorProps,
descriptionProps,
labelProps,
inputContainer,
inputWrapperOrder,
unstyled,
withAsterisk,
variant
}, systemStyles), wrapperProps), /* @__PURE__ */ React.createElement(SelectPopover, {
opened: shouldRenderDropdown,
transitionProps,
shadow: "sm",
withinPortal,
__staticSelector: "MultiSelect",
onDirectionChange: setDirection,
switchDirectionOnFlip,
zIndex,
dropdownPosition,
positionDependencies: [...positionDependencies, _searchValue],
classNames,
styles,
unstyled,
variant
}, /* @__PURE__ */ React.createElement(SelectPopover.Target, null, /* @__PURE__ */ React.createElement("div", {
className: classes.wrapper,
role: "combobox",
"aria-haspopup": "listbox",
"aria-owns": dropdownOpened && shouldRenderDropdown ? `${uuid}-items` : null,
"aria-controls": uuid,
"aria-expanded": dropdownOpened,
onMouseLeave: () => setHovered(-1),
tabIndex: -1
}, /* @__PURE__ */ React.createElement("input", {
type: "hidden",
name,
value: _value.join(","),
form,
disabled
}), /* @__PURE__ */ React.createElement(Input, __spreadValues({
__staticSelector: "MultiSelect",
style: { overflow: "hidden" },
component: "div",
multiline: true,
size,
variant,
disabled,
error,
required,
radius,
icon,
unstyled,
onMouseDown: (event) => {
var _a2;
event.preventDefault();
!disabled && !valuesOverflow.current && setDropdownOpened(!dropdownOpened);
(_a2 = inputRef.current) == null ? void 0 : _a2.focus();
},
classNames: __spreadProps(__spreadValues({}, classNames), {
input: cx({ [classes.input]: !searchable }, classNames == null ? void 0 : classNames.input)
})
}, getSelectRightSectionProps({
theme,
rightSection,
rightSectionWidth,
styles,
size,
shouldClear: clearable && _value.length > 0,
onClear: handleClear,
error,
disabled,
clearButtonProps,
readOnly
})), /* @__PURE__ */ React.createElement("div", {
className: classes.values,
"data-clearable": clearable || void 0
}, selectedItems, /* @__PURE__ */ React.createElement("input", __spreadValues({
ref: useMergedRef(ref, inputRef),
type: "search",
id: uuid,
className: cx(classes.searchInput, {
[classes.searchInputPointer]: !searchable,
[classes.searchInputInputHidden]: !dropdownOpened && _value.length > 0 || !searchable && _value.length > 0,
[classes.searchInputEmpty]: _value.length === 0
}),
onKeyDown: handleInputKeydown,
value: _searchValue,
onChange: handleInputChange,
onFocus: handleInputFocus,
onBlur: handleInputBlur,
readOnly: !searchable || valuesOverflow.current || readOnly,
placeholder: _value.length === 0 ? placeholder : void 0,
disabled,
"data-mantine-stop-propagation": dropdownOpened,
autoComplete: "off",
onCompositionStart: () => setIMEOpen(true),
onCompositionEnd: () => setIMEOpen(false)
}, rest)))))), /* @__PURE__ */ React.createElement(SelectPopover.Dropdown, {
component: dropdownComponent || SelectScrollArea,
maxHeight: maxDropdownHeight,
direction,
id: uuid,
innerRef: scrollableRef,
__staticSelector: "MultiSelect",
classNames,
styles
}, /* @__PURE__ */ React.createElement(SelectItems, {
data: filteredData,
hovered,
classNames,
styles,
uuid,
__staticSelector: "MultiSelect",
onItemHover: setHovered,
onItemSelect: handleItemSelect,
itemsRefs,
itemComponent,
size,
nothingFound,
isItemSelected,
creatable: creatable && !!createLabel,
createLabel,
unstyled,
variant
}))));
});
MultiSelect.displayName = "@mantine/core/MultiSelect";
export { MultiSelect, defaultFilter, defaultShouldCreate };
//# sourceMappingURL=MultiSelect.js.map