UNPKG

@douyinfe/semi-ui

Version:

A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.

443 lines 14.9 kB
import _noop from "lodash/noop"; import _isEqual from "lodash/isEqual"; import React from 'react'; import PropTypes from 'prop-types'; import cls from 'classnames'; import { strings, cssClasses } from '@douyinfe/semi-foundation/lib/es/autoComplete/constants'; import AutoCompleteFoundation from '@douyinfe/semi-foundation/lib/es/autoComplete/foundation'; import { numbers as popoverNumbers } from '@douyinfe/semi-foundation/lib/es/popover/constants'; import { getUuidShort } from '@douyinfe/semi-foundation/lib/es/utils/uuid'; import BaseComponent from '../_base/baseComponent'; import Spin from '../spin'; import Popover from '../popover'; import Input from '../input'; import Trigger from '../trigger'; import Option from './option'; import warning from '@douyinfe/semi-foundation/lib/es/utils/warning'; import '@douyinfe/semi-foundation/lib/es/autoComplete/autoComplete.css'; import { getDefaultPropsFromGlobalConfig } from "../_utils"; const prefixCls = cssClasses.PREFIX; const sizeSet = strings.SIZE; const positionSet = strings.POSITION; const statusSet = strings.STATUS; class AutoComplete extends BaseComponent { constructor(props) { super(props); this.onSelect = (option, optionIndex, e) => { this.foundation.handleSelect(option, optionIndex); }; this.onSearch = value => { this.foundation.handleSearch(value); }; this.onBlur = e => this.foundation.handleBlur(e); this.onFocus = e => this.foundation.handleFocus(e); this.onInputClear = () => this.foundation.handleClear(); this.handleInputClick = e => this.foundation.handleInputClick(e); this.foundation = new AutoCompleteFoundation(this.adapter); const initRePosKey = 1; this.state = { dropdownMinWidth: null, inputValue: '', // option list options: [], // popover visible visible: false, // current focus option index focusIndex: props.defaultActiveFirstOption ? 0 : -1, // current selected options selection: new Map(), rePosKey: initRePosKey }; this.triggerRef = /*#__PURE__*/React.createRef(); this.optionsRef = /*#__PURE__*/React.createRef(); this.optionContainerEl = /*#__PURE__*/React.createRef(); this.clickOutsideHandler = null; this.optionListId = ''; warning('triggerRender' in this.props && typeof this.props.triggerRender === 'function', `[Semi AutoComplete] - If you are using the following props: 'suffix', 'prefix', 'showClear', 'validateStatus', and 'size', please notice that they will be removed in the next major version. Please use 'componentProps' to retrieve these props instead. - If you are using 'onBlur', 'onFocus', please try to avoid using them and look for changes in the future.`); } get adapter() { const keyboardAdapter = { registerKeyDown: cb => { const keyboardEventSet = { onKeyDown: cb }; this.setState({ keyboardEventSet }); }, unregisterKeyDown: cb => { this.setState({ keyboardEventSet: {} }); }, updateFocusIndex: focusIndex => { this.setState({ focusIndex }); }, updateScrollTop: index => { let optionClassName; /** * Unlike Select which needs to process renderOptionItem separately, when renderItem is enabled in autocomplete * the content passed by the user is still wrapped in the selector of .semi-autocomplete-option * so the selector does not need to be judged separately. */ optionClassName = `.${prefixCls}-option-selected`; if (index !== undefined) { optionClassName = `.${prefixCls}-option:nth-child(${index + 1})`; } let destNode = document.querySelector(`#${prefixCls}-${this.optionListId} ${optionClassName}`); if (Array.isArray(destNode)) { destNode = destNode[0]; } if (destNode) { const destParent = destNode.parentNode; destParent.scrollTop = destNode.offsetTop - destParent.offsetTop - destParent.clientHeight / 2 + destNode.clientHeight / 2; } } }; return Object.assign(Object.assign(Object.assign({}, super.adapter), keyboardAdapter), { getTriggerWidth: () => { const el = this.triggerRef.current; return el && el.getBoundingClientRect().width; }, setOptionWrapperWidth: width => { this.setState({ dropdownMinWidth: width }); }, updateInputValue: inputValue => { this.setState({ inputValue }); }, toggleListVisible: isShow => { this.setState({ visible: isShow }); }, updateOptionList: optionList => { this.setState({ options: optionList }); }, updateSelection: selection => { this.setState({ selection }); }, notifySearch: inputValue => { this.props.onSearch(inputValue); }, notifyChange: value => { this.props.onChange(value); }, notifySelect: option => { this.props.onSelect(option); }, notifyDropdownVisibleChange: isVisible => { this.props.onDropdownVisibleChange(isVisible); }, notifyClear: () => { this.props.onClear(); }, notifyFocus: event => { this.props.onFocus(event); }, notifyBlur: event => { this.props.onBlur(event); }, notifyKeyDown: e => { this.props.onKeyDown(e); }, rePositionDropdown: () => { let { rePosKey } = this.state; rePosKey = rePosKey + 1; this.setState({ rePosKey }); }, registerClickOutsideHandler: cb => { const clickOutsideHandler = e => { const triggerDom = this.triggerRef && this.triggerRef.current; const optionsDom = this.optionContainerEl && this.optionContainerEl.current; const target = e.target; const path = e.composedPath && e.composedPath() || [target]; if (optionsDom && (!optionsDom.contains(target) || !optionsDom.contains(target.parentNode)) && triggerDom && !triggerDom.contains(target) && !(path.includes(triggerDom) || path.includes(optionsDom))) { cb(e); } }; this.clickOutsideHandler = clickOutsideHandler; document.addEventListener('mousedown', clickOutsideHandler, false); }, unregisterClickOutsideHandler: () => { if (this.clickOutsideHandler) { document.removeEventListener('mousedown', this.clickOutsideHandler, false); } } }); } componentDidMount() { this.foundation.init(); this.optionListId = getUuidShort(); } componentWillUnmount() { this.foundation.destroy(); } componentDidUpdate(prevProps, prevState) { if (!_isEqual(this.props.data, prevProps.data)) { this.foundation.handleDataChange(this.props.data); } if (this.props.value !== prevProps.value) { this.foundation.handleValueChange(this.props.value); } } renderInput() { const { size, prefix, insetLabel, insetLabelId, suffix, placeholder, style, className, showClear, disabled, triggerRender, validateStatus, autoFocus, value, id, clearIcon } = this.props; const { inputValue, keyboardEventSet, selection } = this.state; const useCustomTrigger = typeof triggerRender === 'function'; const outerProps = Object.assign(Object.assign(Object.assign({ style, className: useCustomTrigger ? cls(className) : cls({ [prefixCls]: true, [`${prefixCls}-disabled`]: disabled }, className), onClick: this.handleInputClick, ref: this.triggerRef, id }, keyboardEventSet), { // tooltip give tabindex 0 to children by default, autoComplete just need the input get focus, so outer div's tabindex set to -1 tabIndex: -1 }), this.getDataAttr(this.props)); const innerProps = { disabled, placeholder, autoFocus: autoFocus, onChange: this.onSearch, onClear: this.onInputClear, 'aria-label': this.props['aria-label'], 'aria-labelledby': this.props['aria-labelledby'], 'aria-invalid': this.props['aria-invalid'], 'aria-errormessage': this.props['aria-errormessage'], 'aria-describedby': this.props['aria-describedby'], 'aria-required': this.props['aria-required'], // TODO: remove in next major version suffix, prefix: prefix || insetLabel, insetLabelId, showClear, validateStatus, size, onBlur: this.onBlur, onFocus: this.onFocus, clearIcon }; return /*#__PURE__*/React.createElement("div", Object.assign({}, outerProps), typeof triggerRender === 'function' ? (/*#__PURE__*/React.createElement(Trigger, Object.assign({}, innerProps, { inputValue: typeof value !== 'undefined' ? value : inputValue, value: Array.from(selection.values()), triggerRender: triggerRender, componentName: "AutoComplete", componentProps: Object.assign({}, this.props) }))) : (/*#__PURE__*/React.createElement(Input, Object.assign({}, innerProps, { value: typeof value !== 'undefined' ? value : inputValue })))); } renderLoading() { const loadingWrapperCls = `${prefixCls}-loading-wrapper`; return /*#__PURE__*/React.createElement("div", { className: loadingWrapperCls }, /*#__PURE__*/React.createElement(Spin, null)); } renderOption(option, optionIndex) { var _a; const { focusIndex } = this.state; const isFocused = optionIndex === focusIndex; return /*#__PURE__*/React.createElement(Option, Object.assign({ showTick: false, onSelect: (v, e) => this.onSelect(v, optionIndex, e), // selected={selection.has(option.label)} focused: isFocused, onMouseEnter: () => this.foundation.handleOptionMouseEnter(optionIndex), key: option.key || option.label + option.value + optionIndex }, option), (_a = option._renderedLabel) !== null && _a !== void 0 ? _a : option.label); } renderOptionList() { const { maxHeight, dropdownStyle, dropdownClassName, loading, emptyContent } = this.props; const { options, dropdownMinWidth } = this.state; const listCls = cls({ [`${prefixCls}-option-list`]: true }, dropdownClassName); let optionsNode; if (options.length === 0) { optionsNode = emptyContent; } else { optionsNode = options.filter(option => option.show).map((option, i) => this.renderOption(option, i)); } const style = Object.assign({ maxHeight: maxHeight, minWidth: dropdownMinWidth }, dropdownStyle); return /*#__PURE__*/React.createElement("div", { ref: this.optionContainerEl, className: listCls, role: "listbox", style: style, id: `${prefixCls}-${this.optionListId}` }, !loading ? optionsNode : this.renderLoading()); } render() { const { position, motion, zIndex, mouseEnterDelay, mouseLeaveDelay, autoAdjustOverflow, stopPropagation, getPopupContainer } = this.props; const { visible, rePosKey } = this.state; const input = this.renderInput(); const optionList = this.renderOptionList(); return /*#__PURE__*/React.createElement(Popover, { mouseEnterDelay: mouseEnterDelay, mouseLeaveDelay: mouseLeaveDelay, autoAdjustOverflow: autoAdjustOverflow, trigger: "custom", motion: motion, visible: visible, content: optionList, position: position, ref: this.optionsRef, // TransformFromCenter TODO: need to confirm zIndex: zIndex, stopPropagation: stopPropagation, getPopupContainer: getPopupContainer, rePosKey: rePosKey }, input); } } AutoComplete.propTypes = { 'aria-label': PropTypes.string, 'aria-labelledby': PropTypes.string, 'aria-invalid': PropTypes.bool, 'aria-errormessage': PropTypes.string, 'aria-describedby': PropTypes.string, 'aria-required': PropTypes.bool, autoFocus: PropTypes.bool, autoAdjustOverflow: PropTypes.bool, className: PropTypes.string, clearIcon: PropTypes.node, children: PropTypes.node, data: PropTypes.array, defaultOpen: PropTypes.bool, defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), defaultActiveFirstOption: PropTypes.bool, disabled: PropTypes.bool, dropdownMatchSelectWidth: PropTypes.bool, dropdownClassName: PropTypes.string, dropdownStyle: PropTypes.object, emptyContent: PropTypes.node, id: PropTypes.string, insetLabel: PropTypes.node, insetLabelId: PropTypes.string, onSearch: PropTypes.func, onSelect: PropTypes.func, onClear: PropTypes.func, onBlur: PropTypes.func, onFocus: PropTypes.func, onChange: PropTypes.func, onKeyDown: PropTypes.func, position: PropTypes.oneOf(positionSet), placeholder: PropTypes.string, prefix: PropTypes.node, onChangeWithObject: PropTypes.bool, onSelectWithObject: PropTypes.bool, renderItem: PropTypes.func, renderSelectedItem: PropTypes.func, suffix: PropTypes.node, showClear: PropTypes.bool, size: PropTypes.oneOf(sizeSet), style: PropTypes.object, stopPropagation: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), maxHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), mouseEnterDelay: PropTypes.number, mouseLeaveDelay: PropTypes.number, motion: PropTypes.oneOfType([PropTypes.bool, PropTypes.func, PropTypes.object]), getPopupContainer: PropTypes.func, triggerRender: PropTypes.func, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), validateStatus: PropTypes.oneOf(statusSet), zIndex: PropTypes.number }; AutoComplete.Option = Option; AutoComplete.__SemiComponentName__ = "AutoComplete"; AutoComplete.defaultProps = getDefaultPropsFromGlobalConfig(AutoComplete.__SemiComponentName__, { stopPropagation: true, motion: true, zIndex: popoverNumbers.DEFAULT_Z_INDEX, position: 'bottomLeft', data: [], showClear: false, size: 'default', onFocus: _noop, onSearch: _noop, onClear: _noop, onBlur: _noop, onSelect: _noop, onChange: _noop, onSelectWithObject: false, onDropdownVisibleChange: _noop, defaultActiveFirstOption: false, dropdownMatchSelectWidth: true, loading: false, maxHeight: 300, validateStatus: 'default', autoFocus: false, emptyContent: null, onKeyDown: _noop // onPressEnter: () => undefined, // defaultOpen: false, }); export default AutoComplete;