@chayns-components/core
Version:
A set of beautiful React components for developing your own applications with chayns.
469 lines (458 loc) • 21.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _chaynsApi = require("chayns-api");
var _react = require("motion/react");
var _react2 = _interopRequireWildcard(require("react"));
var _reactDom = require("react-dom");
var _comboBox = require("../../types/comboBox");
var _calculate = require("../../utils/calculate");
var _environment = require("../../utils/environment");
var _AreaContextProvider = require("../area-provider/AreaContextProvider");
var _Icon = _interopRequireDefault(require("../icon/Icon"));
var _ComboBoxItem = _interopRequireDefault(require("./combobox-item/ComboBoxItem"));
var _ComboBox = require("./ComboBox.styles");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
const ComboBox = ({
bodyWidth,
direction = _comboBox.ComboBoxDirection.RIGHT,
isDisabled = false,
lists,
maxHeight = '280px',
onSelect,
placeholder,
prefix,
container,
selectedItem,
shouldShowBigImage,
shouldShowClearIcon,
shouldShowRoundImage,
onInputFocus,
shouldUseFullWidth = false,
onInputChange,
shouldUseCurrentItemWidth = false,
onInputBlur,
inputValue
}) => {
const [internalSelectedItem, setInternalSelectedItem] = (0, _react2.useState)();
const [isAnimating, setIsAnimating] = (0, _react2.useState)(false);
const [minWidth, setMinWidth] = (0, _react2.useState)(undefined);
const [bodyMinWidth, setBodyMinWidth] = (0, _react2.useState)(0);
const [focusedIndex, setFocusedIndex] = (0, _react2.useState)(null);
const [overflowY, setOverflowY] = (0, _react2.useState)('hidden');
const [translateX, setTranslateX] = (0, _react2.useState)('0px');
const [translateY, setTranslateY] = (0, _react2.useState)('0px');
const [portal, setPortal] = (0, _react2.useState)();
const [internalCoordinates, setInternalCoordinates] = (0, _react2.useState)({
x: 0,
y: 0
});
const [newContainer, setNewContainer] = (0, _react2.useState)(container ?? null);
const [shouldUseTopAlignment, setShouldUseTopAlignment] = (0, _react2.useState)(false);
const isInputFocused = (0, _react2.useRef)(false);
const styledComboBoxElementRef = (0, _react2.useRef)(null);
const contentRef = (0, _react2.useRef)(null);
const functions = (0, _chaynsApi.useFunctions)();
const values = (0, _chaynsApi.useValues)();
const {
browser
} = (0, _chaynsApi.useDevice)();
const isTouch = (0, _environment.getIsTouch)();
const areaProvider = (0, _react2.useContext)(_AreaContextProvider.AreaContext);
const shouldChangeColor = (0, _react2.useMemo)(() => areaProvider.shouldChangeColor ?? false, [areaProvider.shouldChangeColor]);
const shouldDisableActions = (0, _react2.useMemo)(() => {
if (!selectedItem) {
return false;
}
const combinedLists = lists.flatMap(list => list.list);
return combinedLists.length === 1 && combinedLists.some(item => item.value === selectedItem.value);
}, [lists, selectedItem]);
const contentHeight = (0, _react2.useMemo)(() => {
const flatItems = lists.flatMap(list => list.list);
let result = flatItems.length * 36;
if (lists.length > 1) {
result += lists.length * 36;
}
// ToDo: Implement a better solution to also work with percentage values or other units
if (maxHeight.toString().includes('px')) {
const maxHeightValue = parseInt(maxHeight.toString().replace('px', ''), 10);
if (maxHeightValue < result) {
result = maxHeightValue;
}
}
return result;
}, [lists, maxHeight]);
(0, _react2.useEffect)(() => {
if (styledComboBoxElementRef.current && !container) {
const el = styledComboBoxElementRef.current;
const element = el.closest('.dialog-inner') || el.closest('.page-provider') || el.closest('body');
setNewContainer(element);
}
}, [container]);
(0, _react2.useEffect)(() => {
if (container instanceof Element) {
setNewContainer(container);
}
}, [container]);
const handleInputFocus = (0, _react2.useCallback)(event => {
isInputFocused.current = true;
onInputFocus === null || onInputFocus === void 0 || onInputFocus(event);
}, [onInputFocus]);
const handleInputBlur = (0, _react2.useCallback)(event => {
isInputFocused.current = false;
onInputBlur === null || onInputBlur === void 0 || onInputBlur(event);
}, [onInputBlur]);
const handleClick = (0, _react2.useCallback)(event => {
if (styledComboBoxElementRef.current && !styledComboBoxElementRef.current.contains(event.target) && contentRef.current && !contentRef.current.contains(event.target)) {
setIsAnimating(false);
}
}, [styledComboBoxElementRef]);
const handleOpen = (0, _react2.useCallback)(() => {
if (shouldDisableActions) {
return;
}
if (styledComboBoxElementRef.current && newContainer) {
const {
left: comboBoxLeft,
top: comboBoxTop,
height
} = styledComboBoxElementRef.current.getBoundingClientRect();
const {
left,
top,
height: containerHeight
} = newContainer.getBoundingClientRect();
const x = comboBoxLeft - left + newContainer.scrollLeft;
const y = comboBoxTop - top + newContainer.scrollTop;
let useTopAlignment = [_comboBox.ComboBoxDirection.TOP, _comboBox.ComboBoxDirection.TOP_LEFT, _comboBox.ComboBoxDirection.TOP_RIGHT].includes(direction);
const hasBottomAlignment = [_comboBox.ComboBoxDirection.BOTTOM, _comboBox.ComboBoxDirection.BOTTOM_LEFT, _comboBox.ComboBoxDirection.BOTTOM_RIGHT].includes(direction);
if (!hasBottomAlignment && y + height + contentHeight > containerHeight) {
useTopAlignment = true;
setShouldUseTopAlignment(true);
} else {
setShouldUseTopAlignment(false);
}
setInternalCoordinates({
x,
y: useTopAlignment ? y : y + height
});
setIsAnimating(true);
}
}, [shouldDisableActions, newContainer, contentHeight, direction]);
const handleClose = (0, _react2.useCallback)(() => {
setIsAnimating(false);
}, []);
/**
* This function adds an event listener to the document to close the combobox when the user clicks outside of it
*/
(0, _react2.useEffect)(() => {
document.addEventListener('click', handleClick);
return () => {
document.removeEventListener('click', handleClick);
};
}, [handleClick, styledComboBoxElementRef]);
/**
* This function sets the selected item
*/
const handleSetSelectedItem = (0, _react2.useCallback)(itemToSelect => {
if (typeof onSelect === 'function') {
const onSelectResult = onSelect(itemToSelect);
if (onSelectResult === false) {
return;
}
if (onSelectResult instanceof Promise) {
void onSelectResult.then(shouldPreventSelection => {
if (shouldPreventSelection) return;
setInternalSelectedItem(itemToSelect);
setIsAnimating(false);
});
return;
}
}
setInternalSelectedItem(itemToSelect);
setIsAnimating(false);
}, [onSelect]);
const handleClear = (0, _react2.useCallback)(event => {
event.preventDefault();
event.stopPropagation();
handleSetSelectedItem(undefined);
}, [handleSetSelectedItem]);
(0, _react2.useEffect)(() => {
const currentContent = contentRef.current;
if (portal && isAnimating && currentContent) {
const scrollHeight = currentContent.scrollHeight ?? 0;
const maxHeightInPixels = (0, _calculate.getMaxHeightInPixels)(maxHeight, styledComboBoxElementRef.current ?? document.body);
setOverflowY(scrollHeight > maxHeightInPixels ? 'scroll' : 'hidden');
}
}, [isAnimating, maxHeight, portal]);
(0, _react2.useEffect)(() => {
const handleKeyDown = e => {
if (!isAnimating) {
return;
}
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
var _contentRef$current;
e.preventDefault();
const children = (_contentRef$current = contentRef.current) === null || _contentRef$current === void 0 ? void 0 : _contentRef$current.children;
if (children && children.length > 0) {
const newIndex = focusedIndex !== null ? (focusedIndex + (e.key === 'ArrowUp' ? -1 : 1) + children.length) % children.length : 0;
if (focusedIndex !== null) {
const prevElement = children[focusedIndex];
prevElement.tabIndex = -1;
}
setFocusedIndex(newIndex);
const newElement = children[newIndex];
newElement.tabIndex = 0;
newElement.focus();
}
} else if (e.key === 'Enter' && focusedIndex !== null) {
var _contentRef$current2;
const element = (_contentRef$current2 = contentRef.current) === null || _contentRef$current2 === void 0 ? void 0 : _contentRef$current2.children[focusedIndex];
if (!element) {
return;
}
const {
id
} = element;
let newSelectedItem;
lists.some(list => {
newSelectedItem = list.list.find(({
value
}) => String(value) === id.replace('combobox-item__', ''));
return !!newSelectedItem;
});
if (!newSelectedItem) {
return;
}
handleSetSelectedItem(newSelectedItem);
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [focusedIndex, handleSetSelectedItem, isAnimating, lists]);
/**
* This function calculates the greatest width
*/
(0, _react2.useEffect)(() => {
var _styledComboBoxElemen;
const allItems = lists.flatMap(list => list.list);
const hasImage = [selectedItem, ...allItems].some(item => item === null || item === void 0 ? void 0 : item.imageUrl);
const hasIcon = [selectedItem, ...allItems].some(item => item === null || item === void 0 ? void 0 : item.icons);
const parentWidth = ((_styledComboBoxElemen = styledComboBoxElementRef.current) === null || _styledComboBoxElemen === void 0 || (_styledComboBoxElemen = _styledComboBoxElemen.parentElement) === null || _styledComboBoxElemen === void 0 ? void 0 : _styledComboBoxElemen.getBoundingClientRect().width) ?? 0;
const paddingWidth = 20 + 2 + 40 + 40; // padding + border + arrow icon + optional clear icon
const imageWidth = hasImage ? 32 : 0; // image width + gap if images present
const iconWidth = hasIcon ? 40 : 0; // icon width + gap if icons present
let prefixWidth = 0;
if (prefix) {
const prefixTextWidth = (0, _calculate.calculateContentWidth)([{
text: prefix,
value: 'prefix'
}], functions, values) + 5;
prefixWidth = Math.max(prefixTextWidth, 32);
}
const baseWidth = (0, _calculate.calculateContentWidth)([...allItems, {
text: placeholder,
value: 'placeholder'
}, ...(selectedItem ? [selectedItem] : [])], functions, values);
const calculatedWidth = baseWidth + paddingWidth + imageWidth + iconWidth + prefixWidth;
let tmpMinWidth = calculatedWidth;
let tmpBodyMinWidth = calculatedWidth;
// Full width settings
if (shouldUseFullWidth) {
tmpMinWidth = parentWidth;
tmpBodyMinWidth = parentWidth < calculatedWidth - 20 ? calculatedWidth - 20 : parentWidth;
}
// Current item width settings
else if (shouldUseCurrentItemWidth && internalSelectedItem) {
const itemWidth = (0, _calculate.calculateContentWidth)([internalSelectedItem], functions, values) + paddingWidth + imageWidth + iconWidth + prefixWidth;
tmpMinWidth = itemWidth;
tmpBodyMinWidth = itemWidth < calculatedWidth - 20 ? calculatedWidth - 20 : itemWidth;
}
if (tmpMinWidth > parentWidth) {
tmpMinWidth = parentWidth;
}
if (tmpBodyMinWidth > parentWidth) {
tmpBodyMinWidth = parentWidth;
}
setMinWidth(tmpMinWidth);
setBodyMinWidth(shouldUseCurrentItemWidth ? tmpMinWidth : tmpBodyMinWidth);
}, [lists, placeholder, shouldUseFullWidth, shouldUseCurrentItemWidth, internalSelectedItem, prefix, selectedItem, functions, values]);
/**
* This function sets the external selected item
*/
(0, _react2.useEffect)(() => {
setIsAnimating(false);
setInternalSelectedItem(selectedItem);
}, [selectedItem]);
(0, _react2.useEffect)(() => {
if ([_comboBox.ComboBoxDirection.BOTTOM_LEFT, _comboBox.ComboBoxDirection.TOP_LEFT, _comboBox.ComboBoxDirection.LEFT].includes(direction) && typeof bodyWidth === 'number' && typeof minWidth === 'number') {
const difference = minWidth - bodyWidth;
setTranslateX(`${difference}px`);
} else {
setTranslateX('0px');
}
}, [bodyWidth, direction, minWidth]);
const placeholderImageUrl = (0, _react2.useMemo)(() => {
if (selectedItem) {
return selectedItem.imageUrl;
}
if (internalSelectedItem) {
return internalSelectedItem.imageUrl;
}
return undefined;
}, [internalSelectedItem, selectedItem]);
const placeholderIcon = (0, _react2.useMemo)(() => {
if (selectedItem) {
return selectedItem.icons;
}
if (internalSelectedItem) {
return internalSelectedItem.icons;
}
return undefined;
}, [internalSelectedItem, selectedItem]);
/**
* This function resets the placeholder
*/
const placeholderText = (0, _react2.useMemo)(() => {
let text = placeholder;
if (selectedItem) {
text = selectedItem.text;
} else if (internalSelectedItem) {
text = internalSelectedItem.text;
}
return text;
}, [internalSelectedItem, placeholder, selectedItem]);
const shouldShowRoundPlaceholderImage = (0, _react2.useMemo)(() => {
const selectedItemList = lists.find(list => list.list.some(({
value
}) => value === ((selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.value) ?? (internalSelectedItem === null || internalSelectedItem === void 0 ? void 0 : internalSelectedItem.value))));
return (selectedItemList === null || selectedItemList === void 0 ? void 0 : selectedItemList.shouldShowRoundImage) ?? shouldShowRoundImage;
}, [internalSelectedItem === null || internalSelectedItem === void 0 ? void 0 : internalSelectedItem.value, lists, selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.value, shouldShowRoundImage]);
/**
* This function opens the content of the combobox
*/
const handleHeaderClick = (0, _react2.useCallback)(() => {
if (!isDisabled && !isInputFocused.current) {
if (isAnimating) {
handleClose();
} else {
handleOpen();
}
}
}, [handleClose, handleOpen, isAnimating, isDisabled]);
const comboBoxGroups = (0, _react2.useMemo)(() => lists.map(list => /*#__PURE__*/_react2.default.createElement("div", {
key: list.groupName ?? 'default-group'
}, list.groupName && lists.length > 1 && /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledComboBoxTopic, null, list.groupName), list.list.map(item =>
/*#__PURE__*/
// ToDo: Cleanup this - item should be given as a prop to avoid full spreading
_react2.default.createElement(_ComboBoxItem.default, {
icons: item.icons,
id: item.value,
imageBackground: item.imageBackground,
imageUrl: item.imageUrl,
isDisabled: item.isDisabled,
isSelected: selectedItem ? item.value === selectedItem.value : false,
key: item.value,
onSelect: handleSetSelectedItem,
rightElement: item.rightElement,
shouldShowBigImage: shouldShowBigImage,
shouldShowRoundImage: list.shouldShowRoundImage ?? shouldShowRoundImage,
subtext: item.subtext,
suffixElement: item.suffixElement,
text: item.text,
value: item.value,
textStyles: item.textStyles
})))), [handleSetSelectedItem, lists, selectedItem, shouldShowBigImage, shouldShowRoundImage]);
const bodyStyles = (0, _react2.useMemo)(() => ({
left: internalCoordinates.x,
top: internalCoordinates.y
}), [internalCoordinates.x, internalCoordinates.y]);
(0, _react2.useEffect)(() => {
const useTopAlignment = shouldUseTopAlignment || [_comboBox.ComboBoxDirection.TOP, _comboBox.ComboBoxDirection.TOP_LEFT, _comboBox.ComboBoxDirection.TOP_RIGHT].includes(direction);
if (useTopAlignment) {
setTranslateY('-100%');
} else {
setTranslateY('0px');
}
}, [direction, shouldUseTopAlignment]);
(0, _react2.useEffect)(() => {
if (!newContainer) {
return;
}
setPortal(() => /*#__PURE__*/(0, _reactDom.createPortal)(/*#__PURE__*/_react2.default.createElement(_react.AnimatePresence, {
initial: false
}, isAnimating && /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledMotionComboBoxBody, {
$browser: browser === null || browser === void 0 ? void 0 : browser.name,
animate: {
height: 'fit-content',
opacity: 1
},
$overflowY: overflowY,
$translateX: translateX,
$translateY: translateY,
initial: {
height: 0,
opacity: 0
},
exit: {
height: 0,
opacity: 0
},
$maxHeight: maxHeight,
$minWidth: bodyWidth ?? bodyMinWidth,
style: bodyStyles,
$direction: direction,
$shouldUseCurrentItemWidth: shouldUseCurrentItemWidth,
transition: {
duration: 0.2
},
tabIndex: 0,
ref: contentRef
}, comboBoxGroups)), newContainer));
}, [bodyWidth, bodyMinWidth, bodyStyles, browser === null || browser === void 0 ? void 0 : browser.name, comboBoxGroups, newContainer, direction, isAnimating, maxHeight, minWidth, overflowY, shouldUseCurrentItemWidth, translateX, translateY]);
return (0, _react2.useMemo)(() => /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledComboBox, {
ref: styledComboBoxElementRef,
$minWidth: minWidth,
$shouldUseFullWidth: shouldUseFullWidth,
$shouldUseCurrentItemWidth: shouldUseCurrentItemWidth
}, /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledComboBoxHeader, {
$direction: direction,
onClick: handleHeaderClick,
$isOpen: isAnimating,
$isTouch: isTouch,
$isDisabled: isDisabled,
$shouldChangeColor: shouldChangeColor,
$shouldShowBigImage: shouldShowBigImage
}, /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledComboBoxPrefixAndPlaceholderWrapper, null, prefix && /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledComboBoxPrefix, null, prefix), /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledComboBoxPlaceholder, {
$shouldReduceOpacity: !selectedItem && !internalSelectedItem
}, placeholderImageUrl && /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledComboBoxPlaceholderImage, {
src: placeholderImageUrl,
$shouldShowBigImage: shouldShowBigImage,
$shouldShowRoundImage: shouldShowRoundPlaceholderImage
}), placeholderIcon && /*#__PURE__*/_react2.default.createElement(_Icon.default, {
icons: placeholderIcon
}), typeof inputValue === 'string' ? /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledComboBoxInput, {
disabled: isDisabled,
value: inputValue,
onChange: onInputChange,
onBlur: handleInputBlur,
onFocus: handleInputFocus,
placeholder: placeholderText
}) : /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledComboBoxPlaceholderText, null, placeholderText), internalSelectedItem && internalSelectedItem.suffixElement && internalSelectedItem.suffixElement)), shouldShowClearIcon && internalSelectedItem && /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledComboBoxClearIconWrapper, {
onClick: handleClear
}, /*#__PURE__*/_react2.default.createElement(_Icon.default, {
icons: ['fa fa-times']
})), !shouldDisableActions && /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledComboBoxIconWrapper, {
$shouldShowBorderLeft: shouldShowClearIcon === true && internalSelectedItem !== undefined
}, /*#__PURE__*/_react2.default.createElement(_Icon.default, {
icons: ['fa fa-chevron-down']
}))), portal), [minWidth, shouldUseFullWidth, shouldUseCurrentItemWidth, direction, handleHeaderClick, isAnimating, isTouch, isDisabled, shouldChangeColor, shouldShowBigImage, prefix, selectedItem, internalSelectedItem, placeholderImageUrl, shouldShowRoundPlaceholderImage, placeholderIcon, inputValue, onInputChange, handleInputBlur, handleInputFocus, placeholderText, shouldShowClearIcon, handleClear, shouldDisableActions, portal]);
};
ComboBox.displayName = 'ComboBox';
var _default = exports.default = ComboBox;
//# sourceMappingURL=ComboBox.js.map