UNPKG

@sitecore/sc-contenthub-blok

Version:

A UI library for [Sitecore Content Hub](https://doc.sitecore.com/ch/).

293 lines 13.1 kB
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