@sitecore/sc-contenthub-blok
Version:
A UI library for [Sitecore Content Hub](https://doc.sitecore.com/ch/).
293 lines • 13.1 kB
JavaScript
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
import { css, cx } from "@emotion/css";
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
import useIsMounted from "../../customHooks/useIsMounted";
import { blackAlpha, inputMaxWidth, whiteAlpha } from "../../Foundations";
import { isLTR, isRTL } from "../../Foundations/utils";
import { getEmptyImmutableArray, isFunction, } from "../../Foundations/utils";
import { useIsDarkTheme } from "../../useIsDarkTheme";
import { getPopoverOrigin } from "../BlokButtonSelect/BlokButtonSelect";
import { BlokIcon } from "../BlokIcon/BlokIcon";
import { BlokSearchBox } from "../BlokSearchBox/BlokSearchBox";
import { ListItemIcon, ListItemText, MenuItem, Select, Tooltip, Typography, } from "../CoreComponents";
const classes = {
dropDownAutoWidth: css({
width: "auto",
}),
dropdownFullWidth: css({
width: `${inputMaxWidth}px`,
display: "block",
maxWidth: "100%",
overflow: "hidden",
}),
select: css({
backgroundColor: "transparent !important ",
maxWidth: "calc(100% - 40px)",
display: "flex",
alignItems: "center",
}),
blockSelect: css({
backgroundColor: "transparent !important ",
maxWidth: "calc(100% - 40px)",
overflow: "hidden",
textOverflow: "ellipsis",
lineHeight: 1.5,
display: "block !important",
}),
selectIcon: css({
color: blackAlpha[900],
top: "50%",
transform: "translateY(-50%)",
right: isLTR ? "7px !important" : "auto",
left: isRTL ? "7px !important" : "auto",
position: "absolute",
pointerEvents: "none",
cursor: "pointer",
svg: css({
width: "22px !important",
height: "22px !important",
verticalAlign: "middle",
marginInlineStart: "calc(0.375rem * -1)!important",
marginInlineEnd: "calc(0.375rem * -1)!important",
padding: "0.125rem !important",
}),
}),
selectIconOnDark: css({
color: whiteAlpha[900],
}),
checkBox: css({
marginLeft: -"8px",
}),
noneLabel: css({
color: blackAlpha["800"],
}),
noneLabelOnDark: css({
color: whiteAlpha["800"],
}),
transparentDropdownButton: css({
backgroundColor: "transparent",
}),
searchBoxWithButton: css({ margin: "0 0.5rem 0.5rem 0.5rem" }),
dropdownItemIcon: css({
color: blackAlpha[600],
}),
hidden: css({
display: "none",
}),
lightText: css({ color: blackAlpha["500"] }),
iconLabel: css({
marginLeft: "8px",
verticalAlign: "middle",
maxWidth: "100%",
overflow: "hidden",
textOverflow: "ellipsis",
lineHeight: 1.5,
}),
};
function BlokSelect({ id, options = getEmptyImmutableArray(), onChange: onChange_, value, labelMapper = (item) => item, valueMapper = (item) => item, returnValue, placeholder = "Select an option", emptyValueSelectable = false, className, isReadOnly, searchBoxMinAmountOfItems = 8, tooltip = "", showIconAndText = false, hideItemIcons = false, isFullWidth = false, accessibilityId, maxHeight = 250, itemsToDisable = getEmptyImmutableArray(), openMenuByDefault = false, showEmptyDropdown = false, noneLabel = "None", noResultsText = "No results found", clearMessage = "Clear", searchMessage = "Search", }) {
const [filteredOptions, setFilteredOptions] = useState(options);
const [searchFilter, setSearchFilter] = useState("");
const selectRef = useRef(null);
const dropdownClasses = useMemo(() => ({
select: showIconAndText ? classes.select : classes.blockSelect,
}), [showIconAndText]);
const searchBoxRef = useCallback((node) => {
if (node) {
node.focus();
}
}, []);
const [isMenuVisible, setIsMenuVisible] = useState(openMenuByDefault);
const [toolTipIsShown, setToolTipIsShown] = useState(false);
const isMountedRef = useIsMounted();
const isDarkTheme = useIsDarkTheme();
const customDropdownIcon = useCallback(() => {
return (_jsx(BlokIcon, { icon: "chevron-down", classes: [
classes.selectIcon,
isDarkTheme ? classes.selectIconOnDark : null,
] }));
}, [isDarkTheme]);
// When clicking the clear button on the searchbox clear search value and set the list back to the original values.
const onClearSearch = useCallback(() => {
setFilteredOptions(options);
setSearchFilter("");
}, [options]);
// When selecting a value in the dropdown.
const onChange = useCallback(async (event) => {
onClearSearch();
const key = event.target.value;
const newValue = options?.find((item) => valueMapper(item) === key);
if (isFunction(onChange_)) {
if (newValue) {
if (isFunction(returnValue)) {
await onChange_(returnValue(newValue));
}
else {
await onChange_(newValue);
}
}
if (newValue == null) {
if (emptyValueSelectable) {
await onChange_(newValue);
}
}
}
if (isMountedRef.current) {
// Hide the menu.
setIsMenuVisible(false);
}
}, [
emptyValueSelectable,
isMountedRef,
valueMapper,
onChange_,
onClearSearch,
options,
returnValue,
]);
// When clicking the enter key in the search box select the first value in the list and close the menu.
const onEnterPress = useCallback(async () => {
if (filteredOptions.length) {
// Grab the first value in the filtered options list.
const valueToSelect = filteredOptions[0];
if (isFunction(returnValue)) {
await onChange_(returnValue(valueToSelect));
}
else {
await onChange_(valueToSelect);
}
if (isMountedRef.current) {
// Restore the filtered options to the options.
setFilteredOptions(options);
// Hide the menu.
setIsMenuVisible(false);
}
}
}, [filteredOptions, isMountedRef, onChange_, options, returnValue]);
useEffect(() => {
if (!options.length) {
return;
}
if (searchFilter !== "") {
setFilteredOptions(options.filter((option) => labelMapper(option)
.toLocaleLowerCase()
.includes(searchFilter.toLocaleLowerCase())));
}
else {
setFilteredOptions(options);
}
}, [labelMapper, options, searchFilter]);
const hideToolTip = useCallback(() => setToolTipIsShown(false), []);
const showToolTip = useCallback(() => {
if (!isMenuVisible) {
setToolTipIsShown(true);
}
}, [isMenuVisible]);
const renderValue = useCallback((selected) => {
const selectedItem = options.find((item) => valueMapper(item) === selected);
if (!selectedItem) {
return _jsx("span", { children: placeholder ?? noneLabel });
}
/** This can maybe be refined in the future this way we show the selected icon on the search template dropdown. */
if (Object.prototype.hasOwnProperty.call(selectedItem, "icon")) {
return (_jsxs(_Fragment, { children: [_jsx(BlokIcon, { icon: selectedItem.icon, classes: [classes.dropdownItemIcon] }), showIconAndText && (_jsx("span", { className: classes.iconLabel, children: labelMapper(selectedItem) }))] }));
}
return labelMapper(selectedItem);
}, [
options,
labelMapper,
valueMapper,
placeholder,
noneLabel,
showIconAndText,
]);
const onOpen = useCallback(() => {
setIsMenuVisible(true);
hideToolTip();
}, [hideToolTip]);
const filteredItems = useMemo(() => {
return filteredOptions?.map((option, index) => {
const mappedValue = valueMapper(option);
return (option && (_jsxs(MenuItem, { value: mappedValue, onKeyDown: (e) => {
if (e.key === "Escape") {
setIsMenuVisible(false);
setSearchFilter("");
}
}, disabled: itemsToDisable.some((item) => mappedValue === valueMapper(item)), onClick: async () => {
// When there is only one option in the list and label
// and value of the option are the same, clicking on the item will
// not be detected, hence we need this custom click logic.
if (filteredOptions.length !== 1 ||
typeof valueMapper !== "function" ||
typeof labelMapper !== "function" ||
mappedValue !== labelMapper(option)) {
return;
}
if (isFunction(returnValue)) {
await onChange_(returnValue(option));
}
else {
await onChange_(option);
}
if (isMountedRef.current) {
// Restore the filtered options to the options.
setFilteredOptions(options);
// Hide the menu.
setIsMenuVisible(false);
}
}, children: [Object.prototype.hasOwnProperty.call(option, "icon") &&
!hideItemIcons && (_jsx(ListItemIcon, { children: _jsx(BlokIcon, { icon: option.icon }) })), _jsx(ListItemText, { primary: labelMapper(option) })] }, `${mappedValue}-${index}`)));
});
}, [
filteredOptions,
valueMapper,
itemsToDisable,
hideItemIcons,
labelMapper,
returnValue,
isMountedRef,
onChange_,
options,
]);
const searchBox = useMemo(() => options.length > searchBoxMinAmountOfItems && (_jsx(BlokSearchBox, { onSearch: setSearchFilter, onClearSearch: onClearSearch, value: searchFilter, ref: searchBoxRef, onEnterPress: onEnterPress, isInsideDropdown: true, hasGutterBottom: false, clearMessage: clearMessage, searchMessage: searchMessage })), [
clearMessage,
onClearSearch,
onEnterPress,
options.length,
searchBoxMinAmountOfItems,
searchBoxRef,
searchFilter,
searchMessage,
]);
const menuCloseHandler = useCallback(() => {
setIsMenuVisible(false);
setSearchFilter("");
}, []);
if (!options.length && !showEmptyDropdown) {
return null;
}
const selectedValue = (value && valueMapper(value)) || "";
return (_jsx(_Fragment, { children: isReadOnly ? (_jsx("div", { "data-testid": id, children: _jsx(Typography, { variant: "body1", children: typeof value === "object"
? labelMapper(value)
: (value ?? "") }) })) : (_jsx(Tooltip, { title: toolTipIsShown ? tooltip : "", children: _jsxs(Select, { ref: selectRef, "data-testid": id, onChange: onChange, className: cx(className, isFullWidth
? classes.dropdownFullWidth
: classes.dropDownAutoWidth), classes: dropdownClasses, value: filteredOptions.some((option) => valueMapper(option) === selectedValue)
? selectedValue
: searchFilter, open: isMenuVisible, displayEmpty: true, onMouseEnter: showToolTip, onMouseLeave: hideToolTip, onOpen: onOpen, onClose: menuCloseHandler, renderValue: renderValue, labelId: `label-${accessibilityId}`, onKeyDown: (e) => {
e.stopPropagation();
}, variant: "filled", MenuProps: {
anchorOrigin: getPopoverOrigin(selectRef.current, "anchor"),
transformOrigin: getPopoverOrigin(selectRef.current, "transform"),
slotProps: {
paper: {
style: {
maxHeight,
},
},
},
autoFocus: options.length < searchBoxMinAmountOfItems,
}, IconComponent: customDropdownIcon, children: [searchBox, value === null && (_jsx(MenuItem, { value: undefined, className: classes.hidden, children: _jsx("em", { className: classes.lightText, children: placeholder }) })), emptyValueSelectable && searchFilter === "" && (
// We need to set the value to null so that it is picked up correctly.
_jsx(MenuItem, { value: null, children: _jsx("em", { children: noneLabel }) })), !filteredOptions.length && noResultsText, filteredItems] }) })) }));
}
export { BlokSelect };
//# sourceMappingURL=BlokSelect.js.map