UNPKG

@zohodesk/components

Version:

Dot UI is a customizable React component library built to deliver a clean, accessible, and developer-friendly UI experience. It offers a growing set of reusable components designed to align with modern design systems and streamline application development

825 lines (769 loc) 24.3 kB
/**** Libraries ****/ import React, { PureComponent } from 'react'; import { GroupSelect_defaultProps } from "./props/defaultProps"; import { GroupSelect_propTypes } from "./props/propTypes"; /**** Components ****/ import Popup from "../Popup/Popup"; import TextBoxIcon from "../TextBoxIcon/TextBoxIcon"; import Textbox from "../TextBox/TextBox"; import Card, { CardHeader, CardContent, CardFooter } from "../Card/Card"; import Suggestions from "../MultiSelect/Suggestions"; import EmptyState from "../MultiSelect/EmptyState"; import { Icon } from '@zohodesk/icons'; import Loader from '@zohodesk/svg/lib/Loader/Loader'; import DropDownHeading from "../DropDown/DropDownHeading"; import { Container, Box } from "../Layout"; import { getUniqueId } from "../Provider/IdProvider"; import ResponsiveDropBox from "../ResponsiveDropBox/ResponsiveDropBox"; import { ResponsiveReceiver } from "../Responsive/CustomResponsive"; import style from "./Select.module.css"; /**** Methods ****/ import { makeGetGroupSelectOptions, optionIdGrouping, makeGetGroupSelectFilterSuggestions, extractOptionId, extractOptionIdFromJson } from "../utils/dropDownUtils"; import { getIsEmptyValue, scrollTo, debounce, getSearchString, findScrollEnd, getKeyValue } from "../utils/Common"; /* eslint-disable react/no-unused-prop-types */ /* eslint-disable react/sort-prop-types */ /* eslint-disable react/forbid-component-props */ export class GroupSelectComponent extends PureComponent { constructor(props) { super(props); this.getNextAriaId = getUniqueId(this); let { autoSelectDebouneTime = 350, searchDebounceTime = 500 } = props; this.getGroupSelectOptions = makeGetGroupSelectOptions(); this.getFilterSuggestions = makeGetGroupSelectFilterSuggestions(); this.handleGetGroupSelectOptions = this.handleGetGroupSelectOptions.bind(this); this.handleGetSelectedId = this.handleGetSelectedId.bind(this); this.handleFilterSuggestions = this.handleFilterSuggestions.bind(this); this.handleSearch = this.handleSearch.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); this.handleMouseEnter = this.handleMouseEnter.bind(this); this.handleChange = this.handleChange.bind(this); this.togglePopup = this.togglePopup.bind(this); this.handlePopupClose = this.handlePopupClose.bind(this); this.suggestionContainerRef = this.suggestionContainerRef.bind(this); this.suggestionItemRef = this.suggestionItemRef.bind(this); this.searchInputRef = this.searchInputRef.bind(this); this.valueInputRef = this.valueInputRef.bind(this); this.handleSelectFocus = this.handleSelectFocus.bind(this); this.handleClearSearch = this.handleClearSearch.bind(this); this.handleValueInputChange = this.handleValueInputChange.bind(this); this.handleChangeOnType = debounce(this.handleChangeOnType.bind(this), autoSelectDebouneTime); this.handleFetchOptions = this.handleFetchOptions.bind(this); this.handleGetNextOptions = this.handleGetNextOptions.bind(this); this.handleScroll = this.handleScroll.bind(this); this.handleSearchOptions = debounce(this.handleSearchOptions.bind(this), searchDebounceTime); this.valueInputTypeString = ''; this.valueInputSearchString = ''; this.autoSelectSuggestions = []; this.autoSelectIndex = 0; let { revampedGroups, normalizedAllOptions, normalizedFormatOptions, allOptionIds } = this.handleGetGroupSelectOptions(props); let { selectedId, hoverIndex } = this.handleGetSelectedId(props, allOptionIds); this.normalizedAllOptions = normalizedAllOptions; this.normalizedFormatOptions = normalizedFormatOptions; this.state = { revampedGroups, allOptionIds, selectedId, hoverIndex, searchStr: '', isFetchingOptions: false }; this._isMounted = false; } componentDidMount() { this._isMounted = true; } componentDidUpdate(prevProps) { let { groupedOptions, selectedOption, isPopupOpen, needSearch, isSearchClearOnClose } = this.props; let { allOptionIds, hoverIndex, selectedId, searchStr } = this.state; let { suggestionContainer } = this; let newOptionIds = allOptionIds; let newSelectedId = selectedId; if (groupedOptions !== prevProps.groupedOptions) { let { revampedGroups, normalizedAllOptions, allOptionIds, normalizedFormatOptions } = this.handleGetGroupSelectOptions(this.props); this.normalizedAllOptions = normalizedAllOptions; this.normalizedFormatOptions = normalizedFormatOptions; newOptionIds = allOptionIds; this.setState({ revampedGroups, allOptionIds }); } if (selectedOption !== prevProps.selectedOption) { let { selectedId, hoverIndex } = this.handleGetSelectedId(this.props, newOptionIds); newSelectedId = selectedId; this.setState({ selectedId, hoverIndex }); } let { suggestionOptionIds } = this.handleFilterSuggestions(); let hoverId = getIsEmptyValue(suggestionOptionIds[hoverIndex]) ? '' : suggestionOptionIds[hoverIndex]; let selSuggestion = this[`suggestion_${hoverId}`]; isPopupOpen && scrollTo(suggestionContainer, selSuggestion); if (isPopupOpen !== prevProps.isPopupOpen) { if (isPopupOpen) { setTimeout(() => { this.searchInput && this.searchInput.focus({ preventScroll: true }); }, 10); } else { // needSearch && this.valueInput && this.valueInput.focus({preventScroll:true}); isSearchClearOnClose && searchStr && this.handleSearch(''); let hoverIndex = newOptionIds.indexOf(newSelectedId); hoverIndex = hoverIndex >= 0 ? hoverIndex : 0; this.setState({ hoverIndex }); } } } componentWillUnmount() { this._isMounted = false; } handleGetGroupSelectOptions(props) { let { groupedOptions, allowValueFallback } = props; return this.getGroupSelectOptions({ groupedOptions, allowValueFallback }); } handleGetSelectedId(props, allOptionIds) { let { selectedOption, isDefaultSelectValue } = props; let { selected, groupId } = selectedOption; let selectedId = optionIdGrouping(selected, groupId); let selectedIdIndex = allOptionIds.indexOf(selectedId); if (selectedIdIndex === -1) { selectedIdIndex = 0; if (isDefaultSelectValue) { [selectedId] = allOptionIds; } else { selectedId = ''; } } return { selectedId, hoverIndex: selectedIdIndex }; } handleFilterSuggestions() { let { needSearch, needLocalSearch } = this.props; let { revampedGroups, searchStr = '', allOptionIds } = this.state; if (needSearch && searchStr && searchStr.trim().length) { searchStr = getSearchString(searchStr); let { suggestionGroups, suggestionOptionIds } = this.getFilterSuggestions({ revampedGroups, searchStr, needSearch: needLocalSearch }); return { suggestionGroups, suggestionOptionIds }; } return { suggestionGroups: revampedGroups, suggestionOptionIds: allOptionIds }; } handleSearchOptions() { let { onSearch } = this.props; let { searchStr } = this.state; searchStr && this.handleFetchOptions(onSearch, searchStr); } handleSearch(value) { // let { value = '' } = e.target; let { searchStr = '' } = this.state; let { onSearch } = this.props; let searchStrRegex = getSearchString(searchStr); let valueStrRegex = getSearchString(value); let isSearch = searchStrRegex !== valueStrRegex ? true : false; this.setState({ searchStr: value, hoverIndex: 0 }, () => { if (!value) { onSearch && this.handleFetchOptions(onSearch, ''); } else if (isSearch && onSearch) { this.handleSearchOptions(); } }); } handleKeyDown(e) { let { isPopupOpen, isPopupOpenOnEnter, onKeyDown } = this.props; let { hoverIndex } = this.state; let { suggestionOptionIds } = this.handleFilterSuggestions(); let { keyCode } = e; if (!isPopupOpen && !isPopupOpenOnEnter) { onKeyDown && onKeyDown(e); } if (isPopupOpen && (keyCode === 38 || keyCode === 40) && e.preventDefault) { e.preventDefault(); //prevent body scroll } else if (!isPopupOpen && keyCode === 40) { e.preventDefault(); //prevent body scroll this.togglePopup(e); } if (keyCode === 38 && isPopupOpen && suggestionOptionIds.length) { if (hoverIndex === 0) {// hoverIndex = options.length - 1; } else { hoverIndex -= 1; } this.setState({ hoverIndex }); } else if (keyCode === 40 && isPopupOpen && suggestionOptionIds.length) { if (hoverIndex === suggestionOptionIds.length - 1) {// hoverIndex = 0; } else { if (hoverIndex === suggestionOptionIds.length - 3) { this.handleGetNextOptions(); } hoverIndex += 1; } this.setState({ hoverIndex }); } else if (keyCode === 13) { let id = suggestionOptionIds[hoverIndex]; isPopupOpen && this.handleChange(id, null, null, e); !isPopupOpen && isPopupOpenOnEnter && this.togglePopup(e); } else if (keyCode === 27) { this.valueInput && this.valueInput.focus({ preventScroll: true }); // this.handlePopupClose(e); } } handleMouseEnter(id) { let { hoverIndex } = this.state; let { suggestionOptionIds } = this.handleFilterSuggestions(); let newHoverIndex = suggestionOptionIds.indexOf(id); hoverIndex !== newHoverIndex && this.setState({ hoverIndex: newHoverIndex }); } handleChange(id, value, index, e) { e && e.preventDefault && e.preventDefault(); let { onChange, isReadOnly } = this.props; let { normalizedAllOptions } = this; let { id: selected, groupId } = extractOptionId(id) || extractOptionIdFromJson(id, normalizedAllOptions); if (!getIsEmptyValue(id) && !isReadOnly) { onChange && onChange({ groupId, selected }, normalizedAllOptions[id]); this.handlePopupClose(); // this.valueInput && this.valueInput.focus({preventScroll:true}); } } togglePopup(e) { let { togglePopup, isReadOnly, defaultDropBoxPosition } = this.props; !isReadOnly && togglePopup(e, defaultDropBoxPosition ? `${defaultDropBoxPosition}Center` : null); } handlePopupClose(e) { let { closePopupOnly, isPopupOpen } = this.props; this.valueInput && this.valueInput.focus({ preventScroll: true }); isPopupOpen && closePopupOnly(e); } suggestionContainerRef(el) { this.suggestionContainer = el; } suggestionItemRef(el, index, id) { this[`suggestion_${id}`] = el; } searchInputRef(el) { this.searchInput = el; } valueInputRef(el) { let { getRef } = this.props; this.valueInput = el; getRef && getRef(el); } handleSelectFocus(e) { let { target } = e || {}; target && target.setSelectionRange(target, 0); } handleClearSearch() { this.handleSearch(''); setTimeout(() => { this.searchInput && this.searchInput.focus({ preventScroll: true }); }, 1); } handleValueInputChange(e) { let typeString = getKeyValue(e); let { isPopupOpen, autoSelectOnType } = this.props; if (!isPopupOpen && autoSelectOnType) { this.valueInputTypeString += (typeString || '').trim(); this.handleChangeOnType(); } } handleChangeOnType() { let { revampedGroups } = this.state; let typeString = this.valueInputTypeString; this.valueInputTypeString = ''; let changeValue = () => { let id = this.autoSelectSuggestions[this.autoSelectIndex]; let { suggestionOptionIds } = this.handleFilterSuggestions(); if (!getIsEmptyValue(id)) { this.handleChange(id); let hoverIndex = suggestionOptionIds.indexOf(id); this.setState({ hoverIndex }); } }; if (typeString && typeString === this.valueInputSearchString) { if (this.autoSelectIndex < this.autoSelectSuggestions.length - 1) { this.autoSelectIndex += 1; } else { this.autoSelectIndex = 0; } changeValue(); } else if (typeString) { this.valueInputSearchString = typeString; let { suggestionOptionIds } = this.getFilterSuggestions({ revampedGroups, searchStr: typeString, needSearch: true, isStartsWithSearch: true }); this.autoSelectIndex = 0; this.autoSelectSuggestions = suggestionOptionIds; changeValue(); } } handleScroll(e) { let ele = e.target; let isScrollReachedBottom = findScrollEnd(ele); isScrollReachedBottom && this.handleGetNextOptions(); } handleFetchOptions(APICall) { let searchStr = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; // let funcArgs = args.slice(1, args.length); let { isFetchingOptions = false } = this.state; let { _isMounted } = this; if (!isFetchingOptions && APICall) { this.setState({ isFetchingOptions: true }); try { return APICall(searchStr).then(() => { _isMounted && this.setState({ isFetchingOptions: false }); }, () => { _isMounted && this.setState({ isFetchingOptions: false }); }); } catch (e) { _isMounted && this.setState({ isFetchingOptions: false }); } } } handleGetNextOptions() { let { isNextOptions, getNextOptions } = this.props; let { searchStr } = this.state; isNextOptions && getNextOptions && this.handleFetchOptions(getNextOptions, searchStr); } responsiveFunc(_ref) { let { mediaQueryOR } = _ref; return { tabletMode: mediaQueryOR([{ maxWidth: 700 }]) }; } render() { let { isDisabled, isReadOnly, needSearch, emptyMessage, needSelectDownIcon, maxLength, needBorder, placeHolder, defaultDropBoxPosition, searchBoxPlaceHolder, searchEmptyMessage, dataId, dataIdSlctComp, dataIdDownIcon, dataIdSrchEmptyMsg, needResponsive, className, size, title, textBoxSize, textBoxVariant, animationStyle, dropBoxSize, searchBoxSize, getTargetRef, isPopupOpen, position, getContainerRef, isPopupReady, removeClose, isAbsolutePositioningNeeded, positionsOffset, targetOffset, isRestrictScroll, borderColor, needTick, children, getFooter, i18nKeys, htmlId, iconOnHover, isLoading, dataSelectorId, customProps } = this.props; i18nKeys = Object.assign({}, i18nKeys, { emptyText: i18nKeys.emptyText || emptyMessage, searchEmptyText: i18nKeys.searchEmptyText || searchEmptyMessage }); const { TextBoxIcon_i18n, TextBox_ally_label = 'Click to select options' } = i18nKeys; let { selectedId, hoverIndex, searchStr, revampedGroups, isFetchingOptions } = this.state; let { normalizedFormatOptions } = this; let { suggestionGroups, suggestionOptionIds } = this.handleFilterSuggestions(); let { value: selected = '' } = normalizedFormatOptions[selectedId] || {}; let setAriaId = this.getNextAriaId(); let ariaErrorId = this.getNextAriaId(); let { TextBoxIconProps = {}, TextBoxProps = {} } = customProps; return /*#__PURE__*/React.createElement("div", { className: `${style.container} ${style[`box_${size}`]} ${isReadOnly ? style.readonly : ''} ${borderColor === 'transparent' ? style.transparentContainer : ''} ${iconOnHover && (isReadOnly || isDisabled) ? style.iconOnHoverReadonly : iconOnHover && !(isReadOnly || isDisabled) ? style.iconOnHoverStyle : ''}`, "data-id": dataIdSlctComp, "data-test-id": dataIdSlctComp, "data-title": isDisabled ? title : null, "data-selector-id": dataSelectorId }, /*#__PURE__*/React.createElement("div", { className: `${className ? className : ''}`, onClick: isDisabled || isReadOnly ? null : this.togglePopup, ref: getTargetRef, "data-id": `${isDisabled ? `${dataId}_disabled` : isReadOnly ? `${dataId}_readOnly` : dataId}`, "data-test-id": `${isDisabled ? `${dataId}_disabled` : isReadOnly ? `${dataId}_readOnly` : dataId}` }, children ? children : /*#__PURE__*/React.createElement(React.Fragment, null, needSelectDownIcon ? /*#__PURE__*/React.createElement(TextBoxIcon, { isDisabled: isDisabled, iconRotated: isPopupOpen, inputRef: this.valueInputRef, maxLength: maxLength, needBorder: needBorder, onFocus: this.handleSelectFocus, onKeyDown: this.handleKeyDown, placeHolder: placeHolder, isReadOnly: true, size: textBoxSize, value: selected, variant: textBoxVariant, needReadOnlyStyle: isReadOnly ? true : false, dataId: `${dataId}_textBox`, onKeyPress: this.handleValueInputChange, needEffect: isReadOnly || isDisabled ? false : true, borderColor: borderColor, htmlId: htmlId, a11y: { role: 'combobox', ariaControls: setAriaId, ariaExpanded: !isReadOnly && !isDisabled && isPopupOpen ? true : false, ariaHaspopup: true, ariaReadonly: true, ariaActivedescendant: selectedId, ariaOwns: setAriaId }, i18nKeys: TextBoxIcon_i18n, isFocus: isPopupReady, autoComplete: false, ...TextBoxIconProps }, /*#__PURE__*/React.createElement(Container, { align: "both", dataId: dataIdDownIcon }, /*#__PURE__*/React.createElement(Icon, { name: "ZD-down", size: "7", iconClass: style.arrowIcon }))) : /*#__PURE__*/React.createElement(Textbox, { isDisabled: isDisabled, inputRef: this.valueInputRef, maxLength: maxLength, needBorder: needBorder, onFocus: this.handleSelectFocus, onKeyDown: this.handleKeyDown, placeHolder: placeHolder, isReadOnly: true, needEffect: isReadOnly || isDisabled ? false : true, size: textBoxSize, value: selected, variant: textBoxVariant, needReadOnlyStyle: isReadOnly ? true : false, dataId: `${dataId}_textBox`, onKeyPress: this.handleValueInputChange, borderColor: borderColor, htmlId: htmlId, a11y: { role: 'combobox', ariaLabel: TextBox_ally_label, ariaControls: setAriaId, ariaExpanded: !isReadOnly && !isDisabled && isPopupOpen ? true : false, ariaHaspopup: true, ariaReadonly: true, ariaActivedescendant: selectedId, ariaOwns: setAriaId }, autoComplete: false, isFocus: isPopupReady, ...TextBoxProps }))), !isReadOnly && !isDisabled && isPopupOpen ? /*#__PURE__*/React.createElement(ResponsiveReceiver, { query: this.responsiveFunc, responsiveId: "Helmet" }, _ref2 => { let { tabletMode } = _ref2; return /*#__PURE__*/React.createElement(ResponsiveDropBox, { animationStyle: animationStyle, boxPosition: position || `${defaultDropBoxPosition}Center`, getRef: getContainerRef, isActive: isPopupReady, isAnimate: true, isArrow: false, onClick: removeClose, needResponsive: needResponsive, isPadding: false, isAbsolutePositioningNeeded: isAbsolutePositioningNeeded, positionsOffset: positionsOffset, targetOffset: targetOffset, isRestrictScroll: isRestrictScroll, isResponsivePadding: getFooter ? false : true, alignBox: "row", dataId: `${dataId}_suggestionBox` }, isLoading ? /*#__PURE__*/React.createElement(Container, { align: "both", className: style.loader }, /*#__PURE__*/React.createElement(Loader, null)) : /*#__PURE__*/React.createElement(Box, { flexible: true }, /*#__PURE__*/React.createElement(Card, { customClass: style.box, onScroll: this.handleScroll }, needSearch ? /*#__PURE__*/React.createElement(CardHeader, null, /*#__PURE__*/React.createElement("div", { className: `${style.search} ${style[size]}` }, /*#__PURE__*/React.createElement(TextBoxIcon, { inputRef: this.searchInputRef, maxLength: maxLength, onChange: this.handleSearch, onKeyDown: this.handleKeyDown, placeHolder: searchBoxPlaceHolder, size: searchBoxSize, value: searchStr, onClear: this.handleClearSearch, a11y: { ariaControls: setAriaId, ariaAutocomplete: 'list', ariaDescribedby: ariaErrorId }, autoComplete: false, dataId: `${dataId}_search` }))) : null, /*#__PURE__*/React.createElement(CardContent, { shrink: true, customClass: !tabletMode && dropBoxSize ? style[dropBoxSize] : '', eleRef: this.suggestionContainerRef }, suggestionGroups.length ? suggestionGroups.map(group => { let { id: groupId, name: groupName, options } = group; let hoverId = suggestionOptionIds[hoverIndex]; return /*#__PURE__*/React.createElement(React.Fragment, { key: groupId }, groupName && /*#__PURE__*/React.createElement("div", { className: style.groupTitle }, /*#__PURE__*/React.createElement(DropDownHeading, { text: groupName, a11y: { role: 'heading' } })), /*#__PURE__*/React.createElement(Suggestions, { activeId: selectedId, suggestions: options, getRef: this.suggestionItemRef, hoverId: hoverId, onClick: this.handleChange, onMouseEnter: this.handleMouseEnter, selectedOptions: [selectedId], needTick: needTick, needBorder: false, htmlId: setAriaId, a11y: { ariaParentRole: 'listbox', role: 'option' }, dataId: `${dataId}_Options` })); }) : /*#__PURE__*/React.createElement(EmptyState, { options: revampedGroups, searchString: searchStr, suggestions: suggestionGroups, dataId: dataIdSrchEmptyMsg, isLoading: isFetchingOptions, i18nKeys: i18nKeys, htmlId: ariaErrorId }), isFetchingOptions && /*#__PURE__*/React.createElement(Container, { isCover: false, align: "both" }, /*#__PURE__*/React.createElement(Loader, null))), getFooter ? /*#__PURE__*/React.createElement(CardFooter, null, getFooter()) : null))); }) : null); } } GroupSelectComponent.propTypes = GroupSelect_propTypes; GroupSelectComponent.defaultProps = GroupSelect_defaultProps; GroupSelectComponent.displayName = 'GroupSelect'; let GroupSelect = Popup(GroupSelectComponent); GroupSelect.defaultProps = GroupSelectComponent.defaultProps; GroupSelect.propTypes = GroupSelectComponent.propTypes; export default GroupSelect; // if (__DOCS__) { // GroupSelect.docs = { // componentGroup: 'Form Elements', // folderName: 'Style Guide' // }; // // eslint-disable-next-line react/forbid-foreign-prop-types // GroupSelect.propTypes = GroupSelectComponent.propTypes; // }