@mantine/core
Version:
React components library focused on usability, accessibility and developer experience
541 lines (538 loc) • 16.9 kB
JavaScript
import React, { forwardRef, useState, useRef, useEffect } from 'react';
import { useScrollIntoView, useUncontrolled, useDidUpdate, useMergedRef } from '@mantine/hooks';
import { getDefaultZIndex } from '@mantine/styles';
import { groupOptions } from '@mantine/utils';
import { SelectScrollArea } from './SelectScrollArea/SelectScrollArea.js';
import { DefaultItem } from './DefaultItem/DefaultItem.js';
import { getSelectRightSectionProps } from './SelectRightSection/get-select-right-section-props.js';
import { SelectItems } from './SelectItems/SelectItems.js';
import { SelectPopover } from './SelectPopover/SelectPopover.js';
import { filterData } from './filter-data/filter-data.js';
import useStyles from './Select.styles.js';
import { useInputProps } from '../Input/use-input-props.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, item) {
return item.label.toLowerCase().trim().includes(value.toLowerCase().trim());
}
function defaultShouldCreate(query, data) {
return !!query && !data.some((item) => item.label.toLowerCase() === query.toLowerCase());
}
const defaultProps = {
required: false,
size: "sm",
shadow: "sm",
itemComponent: DefaultItem,
transitionProps: { transition: "fade", duration: 0 },
initiallyOpened: false,
filter: defaultFilter,
maxDropdownHeight: 220,
searchable: false,
clearable: false,
limit: Infinity,
disabled: false,
creatable: false,
shouldCreate: defaultShouldCreate,
selectOnBlur: false,
switchDirectionOnFlip: false,
filterDataOnExactSearchMatch: false,
zIndex: getDefaultZIndex("popover"),
positionDependencies: [],
dropdownPosition: "flip"
};
const Select = forwardRef((props, ref) => {
const _a = useInputProps("Select", defaultProps, props), {
inputProps,
wrapperProps,
shadow,
data,
value,
defaultValue,
onChange,
itemComponent,
onKeyDown,
onBlur,
onFocus,
transitionProps,
initiallyOpened,
unstyled,
classNames,
styles,
filter,
maxDropdownHeight,
searchable,
clearable,
nothingFound,
limit,
disabled,
onSearchChange,
searchValue,
rightSection,
rightSectionWidth,
creatable,
getCreateLabel,
shouldCreate,
selectOnBlur,
onCreate,
dropdownComponent,
onDropdownClose,
onDropdownOpen,
withinPortal,
portalProps,
switchDirectionOnFlip,
zIndex,
name,
dropdownPosition,
allowDeselect,
placeholder,
filterDataOnExactSearchMatch,
form,
positionDependencies,
readOnly,
clearButtonProps,
hoverOnSearchChange
} = _a, others = __objRest(_a, [
"inputProps",
"wrapperProps",
"shadow",
"data",
"value",
"defaultValue",
"onChange",
"itemComponent",
"onKeyDown",
"onBlur",
"onFocus",
"transitionProps",
"initiallyOpened",
"unstyled",
"classNames",
"styles",
"filter",
"maxDropdownHeight",
"searchable",
"clearable",
"nothingFound",
"limit",
"disabled",
"onSearchChange",
"searchValue",
"rightSection",
"rightSectionWidth",
"creatable",
"getCreateLabel",
"shouldCreate",
"selectOnBlur",
"onCreate",
"dropdownComponent",
"onDropdownClose",
"onDropdownOpen",
"withinPortal",
"portalProps",
"switchDirectionOnFlip",
"zIndex",
"name",
"dropdownPosition",
"allowDeselect",
"placeholder",
"filterDataOnExactSearchMatch",
"form",
"positionDependencies",
"readOnly",
"clearButtonProps",
"hoverOnSearchChange"
]);
const { classes, cx, theme } = useStyles();
const [dropdownOpened, _setDropdownOpened] = useState(initiallyOpened);
const [hovered, setHovered] = useState(-1);
const inputRef = useRef();
const itemsRefs = useRef({});
const [direction, setDirection] = useState("column");
const isColumn = direction === "column";
const { scrollIntoView, targetRef, scrollableRef } = useScrollIntoView({
duration: 0,
offset: 5,
cancelable: false,
isList: true
});
const isDeselectable = allowDeselect === void 0 ? clearable : allowDeselect;
const setDropdownOpened = (opened) => {
if (dropdownOpened !== opened) {
_setDropdownOpened(opened);
const handler = opened ? onDropdownOpen : onDropdownClose;
typeof handler === "function" && handler();
}
};
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, handleChange, controlled] = useUncontrolled({
value,
defaultValue,
finalValue: null,
onChange
});
const selectedValue = sortedData.find((item) => item.value === _value);
const [inputValue, setInputValue] = useUncontrolled({
value: searchValue,
defaultValue: (selectedValue == null ? void 0 : selectedValue.label) || "",
finalValue: void 0,
onChange: onSearchChange
});
const handleSearchChange = (val) => {
setInputValue(val);
if (searchable && typeof onSearchChange === "function") {
onSearchChange(val);
}
};
const handleClear = () => {
var _a2;
if (!readOnly) {
handleChange(null);
if (!controlled) {
handleSearchChange("");
}
(_a2 = inputRef.current) == null ? void 0 : _a2.focus();
}
};
useEffect(() => {
const newSelectedValue = sortedData.find((item) => item.value === _value);
if (newSelectedValue) {
handleSearchChange(newSelectedValue.label);
} else if (!isCreatable || !_value) {
handleSearchChange("");
}
}, [_value]);
useEffect(() => {
if (selectedValue && (!searchable || !dropdownOpened)) {
handleSearchChange(selectedValue.label);
}
}, [selectedValue == null ? void 0 : selectedValue.label]);
const handleItemSelect = (item) => {
if (!readOnly) {
if (isDeselectable && (selectedValue == null ? void 0 : selectedValue.value) === item.value) {
handleChange(null);
setDropdownOpened(false);
} else {
if (item.creatable && typeof onCreate === "function") {
const createdItem = onCreate(item.value);
if (typeof createdItem !== "undefined" && createdItem !== null) {
if (typeof createdItem === "string") {
handleChange(createdItem);
} else {
handleChange(createdItem.value);
}
}
} else {
handleChange(item.value);
}
if (!controlled) {
handleSearchChange(item.label);
}
setHovered(-1);
setDropdownOpened(false);
inputRef.current.focus();
}
}
};
const filteredData = filterData({
data: sortedData,
searchable,
limit,
searchValue: inputValue,
filter,
filterDataOnExactSearchMatch,
value: _value
});
if (isCreatable && shouldCreate(inputValue, filteredData)) {
createLabel = getCreateLabel(inputValue);
filteredData.push({ label: inputValue, value: inputValue, creatable: true });
}
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 && inputValue) {
setHovered(0);
} else {
setHovered(-1);
}
}, [inputValue, hoverOnSearchChange]);
const selectedItemIndex = _value ? filteredData.findIndex((el) => el.value === _value) : 0;
const shouldShowDropdown = !readOnly && (filteredData.length > 0 ? dropdownOpened : dropdownOpened && !!nothingFound);
const handlePrevious = () => {
setHovered((current) => {
var _a2;
const nextIndex = getNextIndex(current, (index) => index - 1, (index) => index > 0);
targetRef.current = itemsRefs.current[(_a2 = filteredData[nextIndex]) == null ? void 0 : _a2.value];
shouldShowDropdown && scrollIntoView({ alignment: isColumn ? "start" : "end" });
return nextIndex;
});
};
const handleNext = () => {
setHovered((current) => {
var _a2;
const nextIndex = getNextIndex(current, (index) => index + 1, (index) => index < filteredData.length - 1);
targetRef.current = itemsRefs.current[(_a2 = filteredData[nextIndex]) == null ? void 0 : _a2.value];
shouldShowDropdown && scrollIntoView({ alignment: isColumn ? "end" : "start" });
return nextIndex;
});
};
const scrollSelectedItemIntoView = () => window.setTimeout(() => {
var _a2;
targetRef.current = itemsRefs.current[(_a2 = filteredData[selectedItemIndex]) == null ? void 0 : _a2.value];
scrollIntoView({ alignment: isColumn ? "end" : "start" });
}, 0);
useDidUpdate(() => {
if (shouldShowDropdown)
scrollSelectedItemIntoView();
}, [shouldShowDropdown]);
const handleInputKeydown = (event) => {
typeof onKeyDown === "function" && onKeyDown(event);
switch (event.key) {
case "ArrowUp": {
event.preventDefault();
if (!dropdownOpened) {
setHovered(selectedItemIndex);
setDropdownOpened(true);
scrollSelectedItemIntoView();
} else {
isColumn ? handlePrevious() : handleNext();
}
break;
}
case "ArrowDown": {
event.preventDefault();
if (!dropdownOpened) {
setHovered(selectedItemIndex);
setDropdownOpened(true);
scrollSelectedItemIntoView();
} else {
isColumn ? handleNext() : handlePrevious();
}
break;
}
case "Home": {
if (!searchable) {
event.preventDefault();
if (!dropdownOpened) {
setDropdownOpened(true);
}
const firstItemIndex = filteredData.findIndex((item) => !item.disabled);
setHovered(firstItemIndex);
shouldShowDropdown && 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);
shouldShowDropdown && scrollIntoView({ alignment: isColumn ? "end" : "start" });
}
break;
}
case "Escape": {
event.preventDefault();
setDropdownOpened(false);
setHovered(-1);
break;
}
case " ": {
if (!searchable) {
event.preventDefault();
if (filteredData[hovered] && dropdownOpened) {
handleItemSelect(filteredData[hovered]);
} else {
setDropdownOpened(true);
setHovered(selectedItemIndex);
scrollSelectedItemIntoView();
}
}
break;
}
case "Enter": {
if (!searchable) {
event.preventDefault();
}
if (filteredData[hovered] && dropdownOpened) {
event.preventDefault();
handleItemSelect(filteredData[hovered]);
}
}
}
};
const handleInputBlur = (event) => {
typeof onBlur === "function" && onBlur(event);
const selected = sortedData.find((item) => item.value === _value);
if (selectOnBlur && filteredData[hovered] && dropdownOpened) {
handleItemSelect(filteredData[hovered]);
}
handleSearchChange((selected == null ? void 0 : selected.label) || "");
setDropdownOpened(false);
};
const handleInputFocus = (event) => {
typeof onFocus === "function" && onFocus(event);
if (searchable) {
setDropdownOpened(true);
}
};
const handleInputChange = (event) => {
if (!readOnly) {
handleSearchChange(event.currentTarget.value);
if (clearable && event.currentTarget.value === "") {
handleChange(null);
}
setHovered(-1);
setDropdownOpened(true);
}
};
const handleInputClick = () => {
if (!readOnly) {
setDropdownOpened(!dropdownOpened);
if (_value && !dropdownOpened) {
setHovered(selectedItemIndex);
}
}
};
return /* @__PURE__ */ React.createElement(Input.Wrapper, __spreadProps(__spreadValues({}, wrapperProps), {
__staticSelector: "Select"
}), /* @__PURE__ */ React.createElement(SelectPopover, {
opened: shouldShowDropdown,
transitionProps,
shadow: "sm",
withinPortal,
portalProps,
__staticSelector: "Select",
onDirectionChange: setDirection,
switchDirectionOnFlip,
zIndex,
dropdownPosition,
positionDependencies: [...positionDependencies, inputValue],
classNames,
styles,
unstyled,
variant: inputProps.variant
}, /* @__PURE__ */ React.createElement(SelectPopover.Target, null, /* @__PURE__ */ React.createElement("div", {
role: "combobox",
"aria-haspopup": "listbox",
"aria-owns": shouldShowDropdown ? `${inputProps.id}-items` : null,
"aria-controls": inputProps.id,
"aria-expanded": shouldShowDropdown,
onMouseLeave: () => setHovered(-1),
tabIndex: -1
}, /* @__PURE__ */ React.createElement("input", {
type: "hidden",
name,
value: _value || "",
form,
disabled
}), /* @__PURE__ */ React.createElement(Input, __spreadValues(__spreadProps(__spreadValues(__spreadValues({
autoComplete: "off",
type: "search"
}, inputProps), others), {
ref: useMergedRef(ref, inputRef),
onKeyDown: handleInputKeydown,
__staticSelector: "Select",
value: inputValue,
placeholder,
onChange: handleInputChange,
"aria-autocomplete": "list",
"aria-controls": shouldShowDropdown ? `${inputProps.id}-items` : null,
"aria-activedescendant": hovered >= 0 ? `${inputProps.id}-${hovered}` : null,
onMouseDown: handleInputClick,
onBlur: handleInputBlur,
onFocus: handleInputFocus,
readOnly: !searchable || readOnly,
disabled,
"data-mantine-stop-propagation": shouldShowDropdown,
name: null,
classNames: __spreadProps(__spreadValues({}, classNames), {
input: cx({ [classes.input]: !searchable }, classNames == null ? void 0 : classNames.input)
})
}), getSelectRightSectionProps({
theme,
rightSection,
rightSectionWidth,
styles,
size: inputProps.size,
shouldClear: clearable && !!selectedValue,
onClear: handleClear,
error: wrapperProps.error,
clearButtonProps,
disabled,
readOnly
}))))), /* @__PURE__ */ React.createElement(SelectPopover.Dropdown, {
component: dropdownComponent || SelectScrollArea,
maxHeight: maxDropdownHeight,
direction,
id: inputProps.id,
innerRef: scrollableRef,
__staticSelector: "Select",
classNames,
styles
}, /* @__PURE__ */ React.createElement(SelectItems, {
data: filteredData,
hovered,
classNames,
styles,
isItemSelected: (val) => val === _value,
uuid: inputProps.id,
__staticSelector: "Select",
onItemHover: setHovered,
onItemSelect: handleItemSelect,
itemsRefs,
itemComponent,
size: inputProps.size,
nothingFound,
creatable: isCreatable && !!createLabel,
createLabel,
"aria-label": wrapperProps.label,
unstyled,
variant: inputProps.variant
}))));
});
Select.displayName = "@mantine/core/Select";
export { Select, defaultFilter, defaultShouldCreate };
//# sourceMappingURL=Select.js.map