@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.
1,085 lines • 37 kB
JavaScript
import _isObject from "lodash/isObject";
import _noop from "lodash/noop";
import _isNumber from "lodash/isNumber";
import _isFunction from "lodash/isFunction";
import _isEmpty from "lodash/isEmpty";
import _isString from "lodash/isString";
import _isEqual from "lodash/isEqual";
import _isSet from "lodash/isSet";
import React, { Fragment } from 'react';
import cls from 'classnames';
import PropTypes from 'prop-types';
import CascaderFoundation from '@douyinfe/semi-foundation/lib/es/cascader/foundation';
import { cssClasses, strings } from '@douyinfe/semi-foundation/lib/es/cascader/constants';
import { numbers as popoverNumbers } from '@douyinfe/semi-foundation/lib/es/popover/constants';
import '@douyinfe/semi-foundation/lib/es/cascader/cascader.css';
import { IconClear, IconChevronDown } from '@douyinfe/semi-icons';
import { convertDataToEntities, calcMergeType, getKeyByValuePath } from '@douyinfe/semi-foundation/lib/es/cascader/util';
import { calcCheckedKeys, normalizeKeyList, calcDisabledKeys } from '@douyinfe/semi-foundation/lib/es/tree/treeUtil';
import ConfigContext from '../configProvider/context';
import BaseComponent from '../_base/baseComponent';
import Input from '../input/index';
import Popover from '../popover/index';
import Item from './item';
import Trigger from '../trigger';
import Tag from '../tag';
import TagInput from '../tagInput';
import { getDefaultPropsFromGlobalConfig, isSemiIcon } from '../_utils';
const prefixcls = cssClasses.PREFIX;
const resetkey = 0;
class Cascader extends BaseComponent {
constructor(props) {
super(props);
// ref method
this.search = value => {
this.handleInputChange(value);
};
this.handleInputChange = value => {
this.foundation.handleInputChange(value);
};
this.handleTagRemoveInTrigger = pos => {
this.foundation.handleTagRemoveInTrigger(pos);
};
this.handleTagClose = (tagChildren, e, tagKey) => {
// When value has not changed, prevent clicking tag closeBtn to close tag
e.preventDefault();
this.foundation.handleTagRemoveByKey(tagKey);
};
this.renderTagItem = (nodeKey, idx) => {
const {
keyEntities,
disabledKeys
} = this.state;
const {
size,
disabled,
displayProp,
displayRender,
disableStrictly
} = this.props;
if (keyEntities[nodeKey]) {
const isDisabled = disabled || keyEntities[nodeKey].data.disabled || disableStrictly && disabledKeys.has(nodeKey);
const tagCls = cls(`${prefixcls}-selection-tag`, {
[`${prefixcls}-selection-tag-disabled`]: isDisabled
});
// custom render tags
if (_isFunction(displayRender)) {
return displayRender(keyEntities[nodeKey], idx);
// default render tags
} else {
return /*#__PURE__*/React.createElement(Tag, {
size: size === 'default' ? 'large' : size,
key: `tag-${nodeKey}-${idx}`,
color: "white",
tagKey: nodeKey,
className: tagCls,
closable: true,
onClose: this.handleTagClose
}, keyEntities[nodeKey].data[displayProp]);
}
}
return null;
};
this.onRemoveInTagInput = v => {
this.foundation.handleTagRemoveByKey(v);
};
this.handleItemClick = (e, item) => {
this.foundation.handleItemClick(e, item);
};
this.handleItemHover = (e, item) => {
this.foundation.handleItemHover(e, item);
};
this.onItemCheckboxClick = item => {
this.foundation.onItemCheckboxClick(item);
};
this.handleListScroll = (e, ind) => {
this.foundation.handleListScroll(e, ind);
};
this.renderContent = () => {
const {
inputValue,
isSearching,
activeKeys,
selectedKeys,
checkedKeys,
halfCheckedKeys,
loadedKeys,
loadingKeys
} = this.state;
const {
filterTreeNode,
dropdownClassName,
dropdownStyle,
loadData,
emptyContent,
separator,
topSlot,
bottomSlot,
showNext,
multiple,
filterRender,
virtualizeInSearch,
expandIcon
} = this.props;
const searchable = Boolean(filterTreeNode) && isSearching;
const popoverCls = cls(dropdownClassName, `${prefixcls}-popover`);
const renderData = this.foundation.getRenderData();
const isEmpty = !renderData || !renderData.length;
const realDropDownStyle = isEmpty ? Object.assign(Object.assign({}, dropdownStyle), {
minWidth: this.state.emptyContentMinWidth
}) : dropdownStyle;
const content = /*#__PURE__*/React.createElement("div", {
className: popoverCls,
role: "listbox",
style: realDropDownStyle,
onKeyDown: this.foundation.handleKeyDown,
ref: this.optionContainerEl
}, topSlot, /*#__PURE__*/React.createElement(Item, {
activeKeys: activeKeys,
selectedKeys: selectedKeys,
separator: separator,
loadedKeys: loadedKeys,
loadingKeys: loadingKeys,
onItemClick: this.handleItemClick,
onItemHover: this.handleItemHover,
showNext: showNext,
onItemCheckboxClick: this.onItemCheckboxClick,
onListScroll: this.handleListScroll,
searchable: searchable,
keyword: inputValue,
emptyContent: emptyContent,
loadData: loadData,
data: renderData,
multiple: multiple,
checkedKeys: checkedKeys,
halfCheckedKeys: halfCheckedKeys,
filterRender: filterRender,
virtualize: virtualizeInSearch,
expandIcon: expandIcon
}), bottomSlot);
return content;
};
this.renderPlusN = hiddenTag => {
const {
disabled,
showRestTagsPopover,
restTagsPopoverProps
} = this.props;
const plusNCls = cls(`${prefixcls}-selection-n`, {
[`${prefixcls}-selection-n-disabled`]: disabled
});
const renderPlusNChildren = /*#__PURE__*/React.createElement("span", {
className: plusNCls
}, "+", hiddenTag.length);
return showRestTagsPopover ? (/*#__PURE__*/React.createElement(Popover, Object.assign({
content: hiddenTag,
showArrow: true,
trigger: "hover",
position: "top",
autoAdjustOverflow: true
}, restTagsPopoverProps), renderPlusNChildren)) : renderPlusNChildren;
};
this.renderMultipleTags = () => {
const {
autoMergeValue,
maxTagCount,
checkRelation
} = this.props;
const {
checkedKeys,
resolvedCheckedKeys
} = this.state;
const realKeys = this.mergeType === strings.NONE_MERGE_TYPE || checkRelation === strings.UN_RELATED ? checkedKeys : resolvedCheckedKeys;
const displayTag = [];
const hiddenTag = [];
[...realKeys].forEach((checkedKey, idx) => {
const notExceedMaxTagCount = !_isNumber(maxTagCount) || maxTagCount >= idx + 1;
const item = this.renderTagItem(checkedKey, idx);
if (notExceedMaxTagCount) {
displayTag.push(item);
} else {
hiddenTag.push(item);
}
});
return /*#__PURE__*/React.createElement(React.Fragment, null, displayTag, !_isEmpty(hiddenTag) && this.renderPlusN(hiddenTag));
};
this.renderDisplayText = () => {
const {
displayProp,
separator,
displayRender
} = this.props;
const {
selectedKeys
} = this.state;
let displayText = '';
if (selectedKeys.size) {
const displayPath = this.foundation.getItemPropPath([...selectedKeys][0], displayProp);
if (displayRender && typeof displayRender === 'function') {
displayText = displayRender(displayPath);
} else {
displayText = displayPath.map((path, index) => (/*#__PURE__*/React.createElement(Fragment, {
key: `${path}-${index}`
}, index < displayPath.length - 1 ? (/*#__PURE__*/React.createElement(React.Fragment, null, path, separator)) : path)));
}
}
return displayText;
};
this.renderSelectContent = () => {
const {
placeholder,
filterTreeNode,
multiple,
searchPosition
} = this.props;
const {
checkedKeys
} = this.state;
const searchable = Boolean(filterTreeNode) && searchPosition === strings.SEARCH_POSITION_TRIGGER;
if (!searchable) {
if (multiple) {
if (checkedKeys.size === 0) {
return /*#__PURE__*/React.createElement("span", {
className: `${prefixcls}-selection-placeholder`
}, placeholder);
}
return this.renderMultipleTags();
} else {
const displayText = this.renderDisplayText();
const spanCls = cls({
[`${prefixcls}-selection-placeholder`]: !displayText
});
return /*#__PURE__*/React.createElement("span", {
className: spanCls
}, displayText ? displayText : placeholder);
}
}
const input = multiple ? this.renderTagInput() : this.renderInput();
return input;
};
this.renderSuffix = () => {
const {
suffix
} = this.props;
const suffixWrapperCls = cls({
[`${prefixcls}-suffix`]: true,
[`${prefixcls}-suffix-text`]: suffix && _isString(suffix),
[`${prefixcls}-suffix-icon`]: isSemiIcon(suffix)
});
return /*#__PURE__*/React.createElement("div", {
className: suffixWrapperCls,
"x-semi-prop": "suffix"
}, suffix);
};
this.renderPrefix = () => {
const {
prefix,
insetLabel,
insetLabelId
} = this.props;
const labelNode = prefix || insetLabel;
const prefixWrapperCls = cls({
[`${prefixcls}-prefix`]: true,
// to be doublechecked
[`${prefixcls}-inset-label`]: insetLabel,
[`${prefixcls}-prefix-text`]: labelNode && _isString(labelNode),
[`${prefixcls}-prefix-icon`]: isSemiIcon(labelNode)
});
return /*#__PURE__*/React.createElement("div", {
className: prefixWrapperCls,
id: insetLabelId,
"x-semi-prop": "prefix,insetLabel"
}, labelNode);
};
this.renderCustomTrigger = () => {
var _a;
const {
disabled,
triggerRender,
multiple
} = this.props;
const {
selectedKeys,
inputValue,
inputPlaceHolder,
resolvedCheckedKeys,
checkedKeys,
keyEntities
} = this.state;
let realValue;
if (multiple) {
if (this.mergeType === strings.NONE_MERGE_TYPE) {
realValue = new Set();
checkedKeys.forEach(key => {
var _a;
realValue.add((_a = keyEntities[key]) === null || _a === void 0 ? void 0 : _a.pos);
});
} else {
realValue = new Set();
resolvedCheckedKeys.forEach(key => {
var _a;
realValue.add((_a = keyEntities[key]) === null || _a === void 0 ? void 0 : _a.pos);
});
}
} else {
realValue = (_a = keyEntities[[...selectedKeys][0]]) === null || _a === void 0 ? void 0 : _a.pos;
}
return /*#__PURE__*/React.createElement(Trigger, {
value: realValue,
inputValue: inputValue,
onChange: this.handleInputChange,
onClear: this.handleClear,
placeholder: inputPlaceHolder,
disabled: disabled,
triggerRender: triggerRender,
componentName: 'Cascader',
componentProps: Object.assign({}, this.props),
onSearch: this.handleInputChange,
onRemove: this.handleTagRemoveInTrigger
});
};
this.handleMouseOver = () => {
this.foundation.toggleHoverState(true);
};
this.handleMouseLeave = () => {
this.foundation.toggleHoverState(false);
};
this.handleClear = e => {
e && e.stopPropagation();
this.foundation.handleClear();
};
/**
* A11y: simulate clear button click
*/
/* istanbul ignore next */
this.handleClearEnterPress = e => {
e && e.stopPropagation();
this.foundation.handleClearEnterPress(e);
};
this.showClearBtn = () => {
const {
showClear,
disabled,
multiple
} = this.props;
const {
selectedKeys,
isOpen,
isHovering,
checkedKeys,
inputValue
} = this.state;
const hasValue = selectedKeys.size;
const multipleWithHaveValue = multiple && checkedKeys.size;
return showClear && (inputValue || hasValue || multipleWithHaveValue) && !disabled && (isOpen || isHovering);
};
this.renderClearBtn = () => {
const clearCls = cls(`${prefixcls}-clearbtn`);
const {
clearIcon
} = this.props;
const allowClear = this.showClearBtn();
if (allowClear) {
return /*#__PURE__*/React.createElement("div", {
className: clearCls,
onClick: this.handleClear,
onKeyPress: this.handleClearEnterPress,
role: "button",
tabIndex: 0
}, clearIcon ? clearIcon : /*#__PURE__*/React.createElement(IconClear, null));
}
return null;
};
this.renderArrow = () => {
const {
arrowIcon
} = this.props;
const showClearBtn = this.showClearBtn();
if (showClearBtn) {
return null;
}
return arrowIcon ? (/*#__PURE__*/React.createElement("div", {
className: cls(`${prefixcls}-arrow`),
"x-semi-prop": "arrowIcon"
}, arrowIcon)) : null;
};
this.renderSelection = () => {
const {
disabled,
multiple,
filterTreeNode,
style,
size,
className,
validateStatus,
prefix,
suffix,
insetLabel,
triggerRender,
showClear,
id,
borderless
} = this.props;
const {
isOpen,
isFocus,
isInput,
checkedKeys
} = this.state;
const filterable = Boolean(filterTreeNode);
const useCustomTrigger = typeof triggerRender === 'function';
const classNames = useCustomTrigger ? cls(className) : cls(prefixcls, className, {
[`${prefixcls}-borderless`]: borderless,
[`${prefixcls}-focus`]: isFocus || isOpen && !isInput,
[`${prefixcls}-disabled`]: disabled,
[`${prefixcls}-single`]: true,
[`${prefixcls}-filterable`]: filterable,
[`${prefixcls}-error`]: validateStatus === 'error',
[`${prefixcls}-warning`]: validateStatus === 'warning',
[`${prefixcls}-small`]: size === 'small',
[`${prefixcls}-large`]: size === 'large',
[`${prefixcls}-with-prefix`]: prefix || insetLabel,
[`${prefixcls}-with-suffix`]: suffix
});
const mouseEvent = showClear ? {
onMouseEnter: () => this.handleMouseOver(),
onMouseLeave: () => this.handleMouseLeave()
} : {};
const sectionCls = cls(`${prefixcls}-selection`, {
[`${prefixcls}-selection-multiple`]: multiple && !_isEmpty(checkedKeys)
});
const inner = useCustomTrigger ? this.renderCustomTrigger() : [/*#__PURE__*/React.createElement(Fragment, {
key: 'prefix'
}, prefix || insetLabel ? this.renderPrefix() : null), /*#__PURE__*/React.createElement(Fragment, {
key: 'selection'
}, /*#__PURE__*/React.createElement("div", {
className: sectionCls
}, this.renderSelectContent())), /*#__PURE__*/React.createElement(Fragment, {
key: 'suffix'
}, suffix ? this.renderSuffix() : null), /*#__PURE__*/React.createElement(Fragment, {
key: 'clearbtn'
}, this.renderClearBtn()), /*#__PURE__*/React.createElement(Fragment, {
key: 'arrow'
}, this.renderArrow())];
/**
* Reasons for disabling the a11y eslint rule:
* The following attributes(aria-controls,aria-expanded) will be automatically added by Tooltip, no need to declare here
*/
return /*#__PURE__*/React.createElement("div", Object.assign({
className: classNames,
style: style,
ref: this.triggerRef,
onClick: e => this.foundation.handleClick(e),
onKeyPress: e => this.foundation.handleSelectionEnterPress(e),
"aria-invalid": this.props['aria-invalid'],
"aria-errormessage": this.props['aria-errormessage'],
"aria-label": this.props['aria-label'],
"aria-labelledby": this.props['aria-labelledby'],
"aria-describedby": this.props['aria-describedby'],
"aria-required": this.props['aria-required'],
id: id,
onKeyDown: this.foundation.handleKeyDown
}, mouseEvent, {
// eslint-disable-next-line jsx-a11y/role-has-required-aria-props
role: "combobox",
tabIndex: 0
}, this.getDataAttr(this.props)), inner);
};
this.state = {
emptyContentMinWidth: null,
disabledKeys: new Set(),
isOpen: props.defaultOpen,
/* By changing rePosKey, the dropdown position can be refreshed */
rePosKey: resetkey,
/* A data structure for storing cascader data items */
keyEntities: {},
/* Selected and show tick icon */
selectedKeys: new Set([]),
/* The key of the activated node */
activeKeys: new Set([]),
/* The key of the filtered node */
filteredKeys: new Set([]),
/* Value of input box */
inputValue: '',
/* Is searching */
isSearching: false,
/* The placeholder of input box */
inputPlaceHolder: props.searchPlaceholder || props.placeholder,
/* Cache props */
prevProps: {},
/* Is hovering */
isHovering: false,
/* Key of checked node, when multiple */
checkedKeys: new Set([]),
/* Key of half checked node, when multiple */
halfCheckedKeys: new Set([]),
/* Auto merged checkedKeys or leaf checkedKeys, when multiple */
resolvedCheckedKeys: new Set([]),
/* Keys of loaded item */
loadedKeys: new Set(),
/* Keys of loading item */
loadingKeys: new Set(),
/* Mark whether this rendering has triggered asynchronous loading of data */
loading: false,
showInput: false
};
this.options = {};
this.isEmpty = false;
this.mergeType = calcMergeType(props.autoMergeValue, props.leafOnly);
this.inputRef = /*#__PURE__*/React.createRef();
this.triggerRef = /*#__PURE__*/React.createRef();
this.optionsRef = /*#__PURE__*/React.createRef();
this.optionContainerEl = /*#__PURE__*/React.createRef();
this.clickOutsideHandler = null;
this.foundation = new CascaderFoundation(this.adapter);
this.loadingKeysRef = /*#__PURE__*/React.createRef();
this.loadedKeysRef = /*#__PURE__*/React.createRef();
}
get adapter() {
var _this = this;
const filterAdapter = {
updateInputValue: value => {
this.setState({
inputValue: value
});
},
updateInputPlaceHolder: value => {
this.setState({
inputPlaceHolder: value
});
},
focusInput: () => {
const {
preventScroll
} = this.props;
if (this.inputRef && this.inputRef.current) {
// TODO: check the reason
this.inputRef.current.focus({
preventScroll
});
}
},
blurInput: () => {
if (this.inputRef && this.inputRef.current) {
this.inputRef.current.blur();
}
}
};
const cascaderAdapter = {
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: () => {
document.removeEventListener('mousedown', this.clickOutsideHandler, false);
},
rePositionDropdown: () => {
let {
rePosKey
} = this.state;
rePosKey = rePosKey + 1;
this.setState({
rePosKey
});
}
};
return Object.assign(Object.assign(Object.assign(Object.assign({}, super.adapter), filterAdapter), cascaderAdapter), {
setEmptyContentMinWidth: minWidth => {
this.setState({
emptyContentMinWidth: minWidth
});
},
getTriggerWidth: () => {
const el = this.triggerRef.current;
return el && el.getBoundingClientRect().width;
},
updateStates: states => {
this.setState(Object.assign({}, states));
},
openMenu: () => {
this.setState({
isOpen: true
});
},
closeMenu: cb => {
this.setState({
isOpen: false
}, () => {
cb && cb();
});
},
updateSelection: selectedKeys => this.setState({
selectedKeys
}),
notifyChange: value => {
this.props.onChange && this.props.onChange(value);
},
notifySelect: selected => {
this.props.onSelect && this.props.onSelect(selected);
},
notifyOnSearch: input => {
this.props.onSearch && this.props.onSearch(input);
},
notifyFocus: function () {
_this.props.onFocus && _this.props.onFocus(...arguments);
},
notifyBlur: function () {
_this.props.onBlur && _this.props.onBlur(...arguments);
},
notifyDropdownVisibleChange: visible => {
this.props.onDropdownVisibleChange(visible);
},
toggleHovering: bool => {
this.setState({
isHovering: bool
});
},
notifyLoadData: (selectedOpt, callback) => {
const {
loadData
} = this.props;
if (loadData) {
new Promise(resolve => {
loadData(selectedOpt).then(() => {
/** Why update loading status & call callback function in setTimeout?
* loadData func will update treeData, treeData change may trigger
* selectedKeys & activeKeys change. For Loading data asynchronously,
* activeKeys should not change, Its implementation depends on loading
* & loadedKeys. The update time of Loading & loadedKeys(in callback func)
* should be later than the update time of treeData(in loaData func)
* In React 18, we need to use setTimeout to ensure the above time requirements.
* */
setTimeout(() => {
callback();
this.setState({
loading: false
});
resolve();
});
});
});
}
},
notifyOnLoad: (newLoadedKeys, data) => {
const {
onLoad
} = this.props;
onLoad && onLoad(newLoadedKeys, data);
},
notifyListScroll: (e, _ref) => {
let {
panelIndex,
activeNode
} = _ref;
this.props.onListScroll(e, {
panelIndex,
activeNode
});
},
notifyOnExceed: data => this.props.onExceed(data),
notifyClear: () => this.props.onClear(),
toggleInputShow: (showInput, cb) => {
this.setState({
showInput
}, () => {
cb();
});
},
updateFocusState: isFocus => {
this.setState({
isFocus
});
},
updateLoadingKeyRefValue: keys => {
this.loadingKeysRef.current = keys;
},
getLoadingKeyRefValue: () => {
return this.loadingKeysRef.current;
},
updateLoadedKeyRefValue: keys => {
this.loadedKeysRef.current = keys;
},
getLoadedKeyRefValue: () => {
return this.loadedKeysRef.current;
}
});
}
static getDerivedStateFromProps(props, prevState) {
const {
multiple,
value,
defaultValue,
onChangeWithObject,
leafOnly,
autoMergeValue,
checkRelation,
searchPlaceholder,
placeholder
} = props;
const {
prevProps
} = prevState;
let keyEntities = prevState.keyEntities || {};
const newState = {};
const newPlaceholder = searchPlaceholder || placeholder;
if (newPlaceholder !== prevState.inputPlaceHolder) {
newState.inputPlaceHolder = newPlaceholder;
}
const needUpdate = name => {
const firstInProps = _isEmpty(prevProps) && name in props;
const nameHasChange = prevProps && !_isEqual(prevProps[name], props[name]);
return firstInProps || nameHasChange;
};
const needUpdateData = () => {
const firstInProps = !prevProps && 'treeData' in props;
const treeDataHasChange = prevProps && prevProps.treeData !== props.treeData;
return firstInProps || treeDataHasChange;
};
const getRealKeys = (realValue, keyEntities) => {
// normalizedValue is used to save the value in two-dimensional array format
let normalizedValue = [];
if (Array.isArray(realValue)) {
normalizedValue = Array.isArray(realValue[0]) ? realValue : [realValue];
} else {
if (realValue !== undefined) {
normalizedValue = [[realValue]];
}
}
// formatValuePath is used to save value of valuePath
const formatValuePath = [];
normalizedValue.forEach(valueItem => {
const formatItem = onChangeWithObject && _isObject(valueItem[0]) ? valueItem.map(i => i === null || i === void 0 ? void 0 : i.value) : valueItem;
formatItem.length > 0 && formatValuePath.push(formatItem);
});
// formatKeys is used to save key of value
const formatKeys = formatValuePath.reduce((acc, cur) => {
const key = getKeyByValuePath(cur);
keyEntities[key] && acc.push(key);
return acc;
}, []);
return formatKeys;
};
if (multiple) {
const needUpdateTreeData = needUpdate('treeData') || needUpdateData();
const needUpdateValue = needUpdate('value') || _isEmpty(prevProps) && defaultValue;
// when value and treedata need updated
if (needUpdateTreeData || needUpdateValue) {
// update state.keyEntities
if (needUpdateTreeData) {
newState.treeData = props.treeData;
keyEntities = convertDataToEntities(props.treeData);
newState.keyEntities = keyEntities;
}
let realKeys = prevState.checkedKeys;
// when data was updated
if (needUpdateValue) {
const realValue = needUpdate('value') ? value : defaultValue;
realKeys = getRealKeys(realValue, keyEntities);
} else {
// needUpdateValue is false
// if treeData is updated & Cascader is controlled, realKeys should be recalculated
if (needUpdateTreeData && 'value' in props) {
const realValue = value;
realKeys = getRealKeys(realValue, keyEntities);
}
}
if (_isSet(realKeys)) {
realKeys = [...realKeys];
}
if (checkRelation === strings.RELATED) {
const calRes = calcCheckedKeys(realKeys, keyEntities);
const checkedKeys = new Set(calRes.checkedKeys);
const halfCheckedKeys = new Set(calRes.halfCheckedKeys);
// disableStrictly
if (props.disableStrictly) {
newState.disabledKeys = calcDisabledKeys(keyEntities);
}
const isLeafOnlyMerge = calcMergeType(autoMergeValue, leafOnly) === strings.LEAF_ONLY_MERGE_TYPE;
newState.checkedKeys = checkedKeys;
newState.halfCheckedKeys = halfCheckedKeys;
newState.resolvedCheckedKeys = new Set(normalizeKeyList(checkedKeys, keyEntities, isLeafOnlyMerge));
} else {
newState.checkedKeys = new Set(realKeys);
}
newState.prevProps = props;
}
}
return newState;
}
componentDidMount() {
this.foundation.init();
}
componentWillUnmount() {
this.foundation.destroy();
}
componentDidUpdate(prevProps) {
// multiple mode uses getDerivedStateFromProps to sync keyEntities,
// so we also need to sync filteredKeys when treeData updates during searching.
if (this.props.multiple) {
if (!_isEqual(prevProps.treeData, this.props.treeData)) {
this.foundation.recalculateFilteredKeys();
}
return;
}
let isOptionsChanged = false;
if (!_isEqual(prevProps.treeData, this.props.treeData)) {
isOptionsChanged = true;
this.foundation.collectOptions();
}
if (prevProps.value !== this.props.value && !isOptionsChanged) {
this.foundation.handleValueChange(this.props.value);
}
}
renderTagInput() {
const {
size,
disabled,
placeholder,
maxTagCount,
showRestTagsPopover,
restTagsPopoverProps,
checkRelation
} = this.props;
const {
inputValue,
checkedKeys,
keyEntities,
resolvedCheckedKeys,
inputPlaceHolder
} = this.state;
const tagInputcls = cls(`${prefixcls}-tagInput-wrapper`);
const realKeys = this.mergeType === strings.NONE_MERGE_TYPE || checkRelation === strings.UN_RELATED ? checkedKeys : resolvedCheckedKeys;
return /*#__PURE__*/React.createElement(TagInput, {
className: tagInputcls,
ref: this.inputRef,
disabled: disabled,
size: size,
value: [...realKeys],
showRestTagsPopover: showRestTagsPopover,
restTagsPopoverProps: restTagsPopoverProps,
maxTagCount: maxTagCount,
renderTagItem: this.renderTagItem,
inputValue: inputValue,
onInputChange: this.handleInputChange,
// TODO Modify logic, not modify type
onRemove: this.onRemoveInTagInput,
placeholder: inputPlaceHolder,
expandRestTagsOnClick: false
});
}
renderInput() {
const {
size,
disabled
} = this.props;
const inputcls = cls(`${prefixcls}-input`);
const {
inputValue,
inputPlaceHolder,
showInput
} = this.state;
const inputProps = {
disabled,
value: inputValue,
className: inputcls,
onChange: this.handleInputChange
};
const wrappercls = cls({
[`${prefixcls}-search-wrapper`]: true,
[`${prefixcls}-search-wrapper-${size}`]: size !== 'default'
});
const displayText = this.renderDisplayText();
const spanCls = cls({
[`${prefixcls}-selection-placeholder`]: !displayText,
[`${prefixcls}-selection-text-hide`]: showInput && inputValue,
[`${prefixcls}-selection-text-inactive`]: showInput && !inputValue
});
return /*#__PURE__*/React.createElement("div", {
className: wrappercls
}, /*#__PURE__*/React.createElement("span", {
className: spanCls
}, displayText ? displayText : inputPlaceHolder), showInput && /*#__PURE__*/React.createElement(Input, Object.assign({
ref: this.inputRef,
size: size
}, inputProps)));
}
close() {
this.foundation.close();
}
open() {
this.foundation.open();
}
focus() {
this.foundation.focus();
}
blur() {
this.foundation.blur();
}
render() {
const {
zIndex,
getPopupContainer,
autoAdjustOverflow,
stopPropagation,
mouseLeaveDelay,
mouseEnterDelay,
position,
motion,
dropdownMargin
} = this.props;
const {
isOpen,
rePosKey
} = this.state;
const {
direction
} = this.context;
const content = this.renderContent();
const selection = this.renderSelection();
const pos = position !== null && position !== void 0 ? position : direction === 'rtl' ? 'bottomRight' : 'bottomLeft';
return /*#__PURE__*/React.createElement(Popover, {
getPopupContainer: getPopupContainer,
zIndex: zIndex,
motion: motion,
margin: dropdownMargin,
ref: this.optionsRef,
content: content,
visible: isOpen,
trigger: "custom",
rePosKey: rePosKey,
position: pos,
autoAdjustOverflow: autoAdjustOverflow,
stopPropagation: stopPropagation,
mouseLeaveDelay: mouseLeaveDelay,
mouseEnterDelay: mouseEnterDelay,
afterClose: () => this.foundation.updateSearching(false)
}, selection);
}
}
Cascader.__SemiComponentName__ = "Cascader";
Cascader.contextType = ConfigContext;
Cascader.propTypes = {
'aria-labelledby': PropTypes.string,
'aria-invalid': PropTypes.bool,
'aria-errormessage': PropTypes.string,
'aria-describedby': PropTypes.string,
'aria-required': PropTypes.bool,
'aria-label': PropTypes.string,
arrowIcon: PropTypes.node,
borderless: PropTypes.bool,
clearIcon: PropTypes.node,
changeOnSelect: PropTypes.bool,
defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
disabled: PropTypes.bool,
dropdownClassName: PropTypes.string,
dropdownStyle: PropTypes.object,
dropdownMargin: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
emptyContent: PropTypes.node,
motion: PropTypes.bool,
/* show search input, if passed in a function, used as custom filter */
filterTreeNode: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
filterLeafOnly: PropTypes.bool,
placeholder: PropTypes.string,
searchPlaceholder: PropTypes.string,
size: PropTypes.oneOf(strings.SIZE_SET),
style: PropTypes.object,
className: PropTypes.string,
treeData: PropTypes.arrayOf(PropTypes.shape({
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
label: PropTypes.any
})),
treeNodeFilterProp: PropTypes.string,
suffix: PropTypes.node,
prefix: PropTypes.node,
insetLabel: PropTypes.node,
insetLabelId: PropTypes.string,
id: PropTypes.string,
displayProp: PropTypes.string,
displayRender: PropTypes.func,
onChange: PropTypes.func,
onSearch: PropTypes.func,
onSelect: PropTypes.func,
onBlur: PropTypes.func,
onFocus: PropTypes.func,
children: PropTypes.node,
getPopupContainer: PropTypes.func,
zIndex: PropTypes.number,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]),
validateStatus: PropTypes.oneOf(strings.VALIDATE_STATUS),
showNext: PropTypes.oneOf([strings.SHOW_NEXT_BY_CLICK, strings.SHOW_NEXT_BY_HOVER]),
stopPropagation: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
showClear: PropTypes.bool,
defaultOpen: PropTypes.bool,
autoAdjustOverflow: PropTypes.bool,
onDropdownVisibleChange: PropTypes.func,
triggerRender: PropTypes.func,
onListScroll: PropTypes.func,
onChangeWithObject: PropTypes.bool,
bottomSlot: PropTypes.node,
topSlot: PropTypes.node,
multiple: PropTypes.bool,
autoMergeValue: PropTypes.bool,
maxTagCount: PropTypes.number,
showRestTagsPopover: PropTypes.bool,
restTagsPopoverProps: PropTypes.object,
max: PropTypes.number,
separator: PropTypes.string,
onExceed: PropTypes.func,
onClear: PropTypes.func,
loadData: PropTypes.func,
onLoad: PropTypes.func,
loadedKeys: PropTypes.array,
disableStrictly: PropTypes.bool,
leafOnly: PropTypes.bool,
enableLeafClick: PropTypes.bool,
clickToSelect: PropTypes.bool,
preventScroll: PropTypes.bool,
position: PropTypes.string,
searchPosition: PropTypes.string,
remote: PropTypes.bool
};
Cascader.defaultProps = getDefaultPropsFromGlobalConfig(Cascader.__SemiComponentName__, {
borderless: false,
leafOnly: false,
arrowIcon: /*#__PURE__*/React.createElement(IconChevronDown, null),
stopPropagation: true,
motion: true,
defaultOpen: false,
zIndex: popoverNumbers.DEFAULT_Z_INDEX,
showClear: false,
autoClearSearchValue: true,
changeOnSelect: false,
disableStrictly: false,
autoMergeValue: true,
multiple: false,
filterTreeNode: false,
filterLeafOnly: true,
showRestTagsPopover: false,
restTagsPopoverProps: {},
separator: ' / ',
size: 'default',
treeNodeFilterProp: 'label',
displayProp: 'label',
treeData: [],
showNext: strings.SHOW_NEXT_BY_CLICK,
onExceed: _noop,
onClear: _noop,
onDropdownVisibleChange: _noop,
onListScroll: _noop,
enableLeafClick: false,
clickToSelect: false,
'aria-label': 'Cascader',
searchPosition: strings.SEARCH_POSITION_TRIGGER,
checkRelation: strings.RELATED,
remote: false
});
export default Cascader;