@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
563 lines (519 loc) • 15.8 kB
JavaScript
import React, { Component } from 'react';
import { SelectWithIcon_defaultProps } from "./props/defaultProps";
import { SelectWithIcon_propTypes } from "./props/propTypes";
/**** Components ****/
import Popup from "../Popup/Popup";
import TextBoxIcon from "../TextBoxIcon/TextBoxIcon";
import { Icon } from '@zohodesk/icons';
import ListItemWithIcon from "../ListItem/ListItemWithIcon";
import Card, { CardHeader, CardContent } from "../Card/Card";
import EmptyState from "../MultiSelect/EmptyState";
import { Container, Box } from "../Layout";
import { getUniqueId } from "../Provider/IdProvider";
import ResponsiveDropBox from "../ResponsiveDropBox/ResponsiveDropBox";
import { ResponsiveReceiver } from "../Responsive/CustomResponsive";
import Loader from '@zohodesk/svg/lib/Loader/Loader';
/**** Methods ****/
import { scrollTo, findScrollEnd } from "../utils/Common.js";
/**** CSS ****/
import style from "./Select.module.css";
class SelectWithIcon extends Component {
constructor(props) {
super(props);
this.state = {
searchValue: '',
selectedIndex: 0,
options: props.options,
isFetchingOptions: false
};
this._isMounted = false;
this.onSearchClear = this.onSearchClear.bind(this);
this.onSearch = this.onSearch.bind(this);
this.togglePopup = this.togglePopup.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleMouseEnter = this.handleMouseEnter.bind(this);
this.scrollContentRef = this.scrollContentRef.bind(this);
this.itemRef = this.itemRef.bind(this);
this.searchInputRef = this.searchInputRef.bind(this);
this.inputRef = this.inputRef.bind(this);
this.handleScroll = this.handleScroll.bind(this);
this.getNextAriaId = getUniqueId(this);
this.handleGetNextOptions = this.handleGetNextOptions.bind(this);
this.handleFetchOptions = this.handleFetchOptions.bind(this);
}
componentDidMount() {
this._isMounted = true;
}
componentWillUnmount() {
this._isMounted = false;
}
inputRef(el) {
this.input = el;
}
itemRef(ele, index, id) {
this[`suggestion_${id}`] = ele;
}
searchInputRef(el) {
this.searchInput = el;
}
scrollContentRef(el) {
let {
isPopupOpen
} = this.props;
if (isPopupOpen) {
this.optionsContainer = el;
}
}
handleMouseEnter(id, value, index, e) {
this.setState({
selectedIndex: index
});
}
handleKeyDown(e) {
let {
keyCode
} = e;
let {
selectedIndex,
options
} = this.state;
let {
idKey,
valueKey,
isPopupOpen,
selectedId
} = this.props;
if (isPopupOpen && (keyCode === 38 || keyCode === 40) && e.preventDefault) {
e.preventDefault(); //prevent body scroll
} else if (!isPopupOpen && (keyCode === 40 || keyCode === 13)) {
e.preventDefault(); //prevent body scroll
this.togglePopup(e);
options.map((list, index) => {
if (list.id === selectedId) {
this.setState({
selectedIndex: index
});
}
});
}
if (isPopupOpen) {
switch (keyCode) {
case 40:
if (selectedIndex === options.length - 1) {
this.setState({
selectedIndex: 0
});
} else {
if (selectedIndex === options.length - 3) {
this.handleGetNextOptions();
}
this.setState({
selectedIndex: selectedIndex + 1
});
}
break;
case 38:
if (selectedIndex === 0) {
this.setState({
selectedIndex: options.length - 1
});
} else {
this.setState({
selectedIndex: selectedIndex - 1
});
}
break;
case 13:
let option = options[selectedIndex];
this.handleChange && this.handleChange(option[idKey], option[valueKey], selectedIndex, e);
break;
}
}
}
componentDidUpdate(prevProps) {
let {
idKey,
isPopupOpen,
needSearch
} = this.props;
let {
selectedIndex,
options
} = this.state;
if (prevProps.isPopupOpen != isPopupOpen) {
setTimeout(() => {
isPopupOpen ? needSearch ? this.searchInput.focus({
preventScroll: true
}) : this.input.focus({
preventScroll: true
}) : this.input.focus({
preventScroll: true
});
}, 10);
}
let option = options[selectedIndex];
let id = option && option[idKey] || {};
let selSuggestion = this[`suggestion_${id}`];
if (isPopupOpen) {
this.optionsContainer && scrollTo(this.optionsContainer, selSuggestion);
}
}
searchList(searchValue) {
let datas = [];
let {
options,
valueKey
} = this.props;
if (options.length) {
datas = options.filter(obj => obj[valueKey].toLowerCase().includes(searchValue.toLowerCase().trim()));
}
return datas;
}
onSearchClear() {
let options = this.searchList(''); // this.searchInput.focus({preventScroll:false});
this.setState({
searchValue: '',
options
});
}
onSearch(searchValue) {
let options = this.searchList(searchValue);
this.setState({
searchValue,
selectedIndex: -1,
options
});
}
togglePopup(e) {
let {
togglePopup,
isReadOnly,
boxPosition
} = this.props;
!isReadOnly && togglePopup(e, boxPosition);
}
handleChange(id, value, index, e) {
let {
onChange,
isReadOnly
} = this.props;
!isReadOnly && onChange && onChange(id, value, index, e);
this.togglePopup(e);
}
handleScroll(e) {
let ele = e.target;
let isScrollReachedBottom = findScrollEnd(ele);
isScrollReachedBottom && this.handleGetNextOptions();
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.options.length != this.props.options.length) {
this.setState({
options: nextProps.options
});
}
}
handleGetNextOptions() {
let {
isNextOptions,
getNextOptions
} = this.props;
let {
searchValue
} = this.state;
isNextOptions && getNextOptions && this.handleFetchOptions(getNextOptions, searchValue);
}
handleFetchOptions(APICall) {
let searchValue = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
let {
isFetchingOptions = false
} = this.state;
let {
_isMounted
} = this;
if (!isFetchingOptions && APICall) {
this.setState({
isFetchingOptions: true
});
try {
return APICall(searchValue).then(() => {
_isMounted && this.setState({
isFetchingOptions: false
});
}, () => {
_isMounted && this.setState({
isFetchingOptions: false
});
});
} catch (e) {
_isMounted && this.setState({
isFetchingOptions: false
});
}
}
}
responsiveFunc(_ref) {
let {
mediaQueryOR
} = _ref;
return {
tabletMode: mediaQueryOR([{
maxWidth: 700
}])
};
}
render() {
let {
animationStyle,
removeClose,
selectedValue,
searchBoxPlaceHolder,
needSearch,
searchEmptyMessage,
dataId,
maxLength,
isDisabled,
isReadOnly,
placeHolder,
textBoxSize,
textBoxVariant,
selectedId,
isPopupOpen,
isPopupReady,
position,
getContainerRef,
isAbsolutePositioningNeeded,
positionsOffset,
targetOffset,
isRestrictScroll,
valueKey,
idKey,
needListBorder,
needTick,
size,
borderColor,
title,
className,
needBorder,
searchBoxSize,
getTargetRef,
needResponsive,
boxSize,
dropBoxSize,
emptyMessage,
needIcon,
iconName,
iconSize,
iconClass,
i18nKeys,
htmlId,
isLoading,
dataSelectorId,
customProps
} = this.props;
i18nKeys = Object.assign({}, i18nKeys, {
emptyText: i18nKeys.emptyText || emptyMessage,
searchEmptyText: i18nKeys.searchEmptyText || searchEmptyMessage
});
let {
searchValue,
selectedIndex,
options,
isFetchingOptions
} = this.state;
let setAriaId = this.getNextAriaId();
let ariaErrorId = this.getNextAriaId();
let {
TextBoxProps = {},
DropdownSearchTextBoxProps = {},
TextBoxIconProps = {}
} = customProps;
return /*#__PURE__*/React.createElement("div", {
className: `${style.container} ${style[`box_${size}`]} ${isReadOnly ? style.readonly : ''} ${borderColor === 'transparent' ? style.transparentContainer : ''}`,
"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}`
}, /*#__PURE__*/React.createElement(Container, {
alignBox: "row"
}, needIcon ? /*#__PURE__*/React.createElement(Container, {
align: "both",
className: `${style.leftIcon} ${isDisabled ? style.disable : ''}`
}, /*#__PURE__*/React.createElement(Icon, {
name: iconName,
size: iconSize,
iconClass: iconClass,
dataId: `${dataId}_icon`
})) : null, /*#__PURE__*/React.createElement(Box, {
flexible: true
}, /*#__PURE__*/React.createElement(TextBoxIcon, {
a11y: {
role: 'combobox',
ariaLabel: 'click to select options',
ariaControls: setAriaId,
ariaExpanded: !isReadOnly && !isDisabled && isPopupOpen ? true : false,
ariaHaspopup: true,
ariaReadonly: true,
ariaActivedescendant: selectedId,
ariaOwns: setAriaId
},
htmlId: htmlId,
value: selectedValue,
isReadOnly: true,
needReadOnlyStyle: isReadOnly ? true : false,
dataId: `${dataId}_textBox`,
maxLength: maxLength,
needBorder: needBorder,
isDisabled: isDisabled,
placeHolder: placeHolder,
size: textBoxSize,
variant: textBoxVariant,
iconRotated: isPopupOpen,
inputRef: this.inputRef,
onKeyDown: this.handleKeyDown,
isClickable: isReadOnly || isDisabled ? false : true,
customClass: {
customTBoxWrap: `${isReadOnly || isDisabled ? '' : style.input} ${needIcon ? style.iconSelect : ''}`
},
needEffect: isReadOnly || isDisabled ? false : true,
borderColor: borderColor,
autoComplete: false,
isFocus: isPopupReady,
customProps: {
TextBoxProps: {
'data-title': title || selectedValue,
...TextBoxProps
}
},
...TextBoxIconProps
}, /*#__PURE__*/React.createElement(Container, {
isInline: true,
isCover: false,
align: "both",
dataId: `${dataId}_downIcon`,
alignBox: "both",
className: style.rightPlaceholder
}, /*#__PURE__*/React.createElement(Icon, {
name: "ZD-down",
size: "7"
})))))), !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,
isActive: isPopupReady,
isAnimate: true,
isArrow: false,
onClick: removeClose,
getRef: getContainerRef,
needResponsive: needResponsive,
isAbsolutePositioningNeeded: isAbsolutePositioningNeeded,
positionsOffset: positionsOffset,
targetOffset: targetOffset,
isRestrictScroll: isRestrictScroll,
dataId: `${dataId}_suggestions`,
size: boxSize,
isPadding: false,
isResponsivePadding: true,
alignBox: "row"
}, 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, {
onScroll: this.handleScroll,
htmlId: setAriaId,
a11y: {
role: 'listbox'
}
}, needSearch ? /*#__PURE__*/React.createElement(CardHeader, null, /*#__PURE__*/React.createElement("div", {
className: `${style.search} ${style[size]}`
}, /*#__PURE__*/React.createElement(TextBoxIcon, {
onClear: this.onSearchClear,
onChange: this.onSearch,
placeHolder: searchBoxPlaceHolder,
value: searchValue,
maxLength: maxLength,
onKeyDown: this.handleKeyDown,
inputRef: this.searchInputRef,
size: searchBoxSize,
dataId: `${dataId}_search`,
a11y: {
ariaDescribedby: ariaErrorId
},
autoComplete: false,
customProps: {
TextBoxProps: DropdownSearchTextBoxProps
}
}))) : null, /*#__PURE__*/React.createElement(CardContent, {
customClass: `${tabletMode ? style.responsivedropBoxList : style.dropBoxList} ${!tabletMode && dropBoxSize ? style[dropBoxSize] : ''}`,
shrink: true,
eleRef: this.scrollContentRef,
dataId: `${dataId}_Options`
}, options.length ? /*#__PURE__*/React.createElement(React.Fragment, null, options.map((options, i) => {
let {
iconName,
iconSize,
iconColor
} = options;
return /*#__PURE__*/React.createElement(ListItemWithIcon, {
key: options[idKey],
value: options[valueKey],
size: "medium",
onClick: this.handleChange,
id: options[idKey],
index: i,
title: options[valueKey],
palette: "default",
dataId: options[idKey],
iconName: iconName,
iconSize: iconSize,
iconClass: iconColor,
active: selectedId === options[idKey],
needBorder: needListBorder,
needTick: needTick,
onMouseEnter: this.handleMouseEnter,
highlight: selectedIndex === i,
getRef: this.itemRef,
a11y: {
role: 'option',
ariaSelected: selectedId === options[idKey],
ariaLabel: options[valueKey]
}
});
})) : /*#__PURE__*/React.createElement(EmptyState, {
isLoading: isFetchingOptions,
options: options,
searchString: searchValue,
suggestions: options,
dataId: dataId,
i18nKeys: i18nKeys,
htmlId: ariaErrorId
}), isFetchingOptions && /*#__PURE__*/React.createElement(Container, {
isCover: false,
align: "both"
}, /*#__PURE__*/React.createElement(Loader, null))))));
}) : null);
}
}
SelectWithIcon.propTypes = SelectWithIcon_propTypes;
SelectWithIcon.defaultProps = SelectWithIcon_defaultProps;
const SelectWithIconComponent = Popup(SelectWithIcon);
SelectWithIconComponent.defaultProps = SelectWithIcon.defaultProps;
SelectWithIconComponent.propTypes = SelectWithIcon.propTypes; // if (__DOCS__) {
// SelectWithIcon.docs = {
// componentGroup: 'Form Elements',
// folderName: 'Style Guide'
// };
// }
export default SelectWithIconComponent;