@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
JavaScript
/**** 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;
// }