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

563 lines (519 loc) 15.8 kB
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;