index.tsx 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948
  1. import React, { Fragment, ReactNode, CSSProperties, MouseEvent } from 'react';
  2. import ReactDOM from 'react-dom';
  3. import cls from 'classnames';
  4. import PropTypes from 'prop-types';
  5. import CascaderFoundation, {
  6. /* Corresponding to the state of react */
  7. BasicCascaderInnerData,
  8. /* Corresponding to the props of react */
  9. BasicCascaderProps,
  10. BasicTriggerRenderProps,
  11. BasicScrollPanelProps,
  12. CascaderAdapter,
  13. CascaderType
  14. } from '@douyinfe/semi-foundation/cascader/foundation';
  15. import { cssClasses, strings } from '@douyinfe/semi-foundation/cascader/constants';
  16. import { numbers as popoverNumbers } from '@douyinfe/semi-foundation/popover/constants';
  17. import { isSet, isEqual, isString, isEmpty, isFunction, isNumber, noop, flatten } from 'lodash';
  18. import '@douyinfe/semi-foundation/cascader/cascader.scss';
  19. import { IconClear, IconChevronDown } from '@douyinfe/semi-icons';
  20. import { findKeysForValues, convertDataToEntities, calcMergeType } from '@douyinfe/semi-foundation/cascader/util';
  21. import { calcCheckedKeys, normalizeKeyList, calcDisabledKeys } from '@douyinfe/semi-foundation/tree/treeUtil';
  22. import ConfigContext from '../configProvider/context';
  23. import BaseComponent, { ValidateStatus } from '../_base/baseComponent';
  24. import Input from '../input/index';
  25. import Popover, { PopoverProps } from '../popover/index';
  26. import Item, { CascaderData, Entities, Entity, Data } from './item';
  27. import Trigger from '../trigger';
  28. import Tag from '../tag';
  29. import TagInput from '../tagInput';
  30. import { Motion } from '../_base/base';
  31. import { isSemiIcon } from '../_utils';
  32. export { CascaderType, ShowNextType } from '@douyinfe/semi-foundation/cascader/foundation';
  33. export { CascaderData, Entity, Data, CascaderItemProps } from './item';
  34. export interface ScrollPanelProps extends BasicScrollPanelProps {
  35. activeNode: CascaderData;
  36. }
  37. export interface TriggerRenderProps extends BasicTriggerRenderProps {
  38. componentProps: CascaderProps;
  39. onClear: (e: React.MouseEvent) => void;
  40. }
  41. /* The basic type of the value of Cascader */
  42. export type SimpleValueType = string | number | CascaderData;
  43. /* The value of Cascader */
  44. export type Value = SimpleValueType | Array<SimpleValueType> | Array<Array<SimpleValueType>>;
  45. export interface CascaderProps extends BasicCascaderProps {
  46. 'aria-describedby'?: React.AriaAttributes['aria-describedby'];
  47. 'aria-errormessage'?: React.AriaAttributes['aria-errormessage'];
  48. 'aria-invalid'?: React.AriaAttributes['aria-invalid'];
  49. 'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
  50. 'aria-required'?: React.AriaAttributes['aria-required'];
  51. arrowIcon?: ReactNode;
  52. defaultValue?: Value;
  53. dropdownStyle?: CSSProperties;
  54. emptyContent?: ReactNode;
  55. motion?: Motion;
  56. treeData?: Array<CascaderData>;
  57. restTagsPopoverProps?: PopoverProps;
  58. children?: ReactNode;
  59. value?: Value;
  60. prefix?: ReactNode;
  61. suffix?: ReactNode;
  62. id?: string;
  63. insetLabel?: ReactNode;
  64. insetLabelId?: string;
  65. style?: CSSProperties;
  66. bottomSlot?: ReactNode;
  67. topSlot?: ReactNode;
  68. triggerRender?: (props: TriggerRenderProps) => ReactNode;
  69. onListScroll?: (e: React.UIEvent<HTMLUListElement, UIEvent>, panel: ScrollPanelProps) => void;
  70. loadData?: (selectOptions: CascaderData[]) => Promise<void>;
  71. onLoad?: (newLoadedKeys: Set<string>, data: CascaderData) => void;
  72. onChange?: (value: Value) => void;
  73. onExceed?: (checkedItem: Entity[]) => void;
  74. displayRender?: (selected: Array<string> | Entity, idx?: number) => ReactNode;
  75. onBlur?: (e: MouseEvent) => void;
  76. onFocus?: (e: MouseEvent) => void;
  77. validateStatus?: ValidateStatus;
  78. }
  79. export interface CascaderState extends BasicCascaderInnerData {
  80. keyEntities: Entities;
  81. prevProps: CascaderProps;
  82. treeData?: Array<CascaderData>;
  83. }
  84. const prefixcls = cssClasses.PREFIX;
  85. const resetkey = 0;
  86. class Cascader extends BaseComponent<CascaderProps, CascaderState> {
  87. static contextType = ConfigContext;
  88. static propTypes = {
  89. 'aria-labelledby': PropTypes.string,
  90. 'aria-invalid': PropTypes.bool,
  91. 'aria-errormessage': PropTypes.string,
  92. 'aria-describedby': PropTypes.string,
  93. 'aria-required': PropTypes.bool,
  94. arrowIcon: PropTypes.node,
  95. changeOnSelect: PropTypes.bool,
  96. defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
  97. disabled: PropTypes.bool,
  98. dropdownClassName: PropTypes.string,
  99. dropdownStyle: PropTypes.object,
  100. emptyContent: PropTypes.node,
  101. motion: PropTypes.oneOfType([PropTypes.bool, PropTypes.func, PropTypes.object]),
  102. /* show search input, if passed in a function, used as custom filter */
  103. filterTreeNode: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
  104. filterLeafOnly: PropTypes.bool,
  105. placeholder: PropTypes.string,
  106. searchPlaceholder: PropTypes.string,
  107. size: PropTypes.oneOf<CascaderType>(strings.SIZE_SET),
  108. style: PropTypes.object,
  109. className: PropTypes.string,
  110. treeData: PropTypes.arrayOf(
  111. PropTypes.shape({
  112. value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  113. label: PropTypes.any,
  114. }),
  115. ),
  116. treeNodeFilterProp: PropTypes.string,
  117. suffix: PropTypes.node,
  118. prefix: PropTypes.node,
  119. insetLabel: PropTypes.node,
  120. insetLabelId: PropTypes.string,
  121. id: PropTypes.string,
  122. displayProp: PropTypes.string,
  123. displayRender: PropTypes.func,
  124. onChange: PropTypes.func,
  125. onSearch: PropTypes.func,
  126. onSelect: PropTypes.func,
  127. onBlur: PropTypes.func,
  128. onFocus: PropTypes.func,
  129. children: PropTypes.node,
  130. getPopupContainer: PropTypes.func,
  131. zIndex: PropTypes.number,
  132. value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]),
  133. validateStatus: PropTypes.oneOf<CascaderProps['validateStatus']>(strings.VALIDATE_STATUS),
  134. showNext: PropTypes.oneOf([strings.SHOW_NEXT_BY_CLICK, strings.SHOW_NEXT_BY_HOVER]),
  135. stopPropagation: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
  136. showClear: PropTypes.bool,
  137. defaultOpen: PropTypes.bool,
  138. autoAdjustOverflow: PropTypes.bool,
  139. onDropdownVisibleChange: PropTypes.func,
  140. triggerRender: PropTypes.func,
  141. onListScroll: PropTypes.func,
  142. onChangeWithObject: PropTypes.bool,
  143. bottomSlot: PropTypes.node,
  144. topSlot: PropTypes.node,
  145. multiple: PropTypes.bool,
  146. autoMergeValue: PropTypes.bool,
  147. maxTagCount: PropTypes.number,
  148. showRestTagsPopover: PropTypes.bool,
  149. restTagsPopoverProps: PropTypes.object,
  150. max: PropTypes.number,
  151. separator: PropTypes.string,
  152. onExceed: PropTypes.func,
  153. onClear: PropTypes.func,
  154. loadData: PropTypes.func,
  155. onLoad: PropTypes.func,
  156. loadedKeys: PropTypes.array,
  157. disableStrictly: PropTypes.bool,
  158. leafOnly: PropTypes.bool,
  159. enableLeafClick: PropTypes.bool,
  160. };
  161. static defaultProps = {
  162. leafOnly: false,
  163. arrowIcon: <IconChevronDown />,
  164. stopPropagation: true,
  165. motion: true,
  166. defaultOpen: false,
  167. zIndex: popoverNumbers.DEFAULT_Z_INDEX,
  168. showClear: false,
  169. autoClearSearchValue: true,
  170. changeOnSelect: false,
  171. disabled: false,
  172. disableStrictly: false,
  173. autoMergeValue: true,
  174. multiple: false,
  175. filterTreeNode: false,
  176. filterLeafOnly: true,
  177. showRestTagsPopover: false,
  178. restTagsPopoverProps: {},
  179. separator: ' / ',
  180. size: 'default' as const,
  181. treeNodeFilterProp: 'label' as const,
  182. displayProp: 'label' as const,
  183. treeData: [] as Array<CascaderData>,
  184. showNext: strings.SHOW_NEXT_BY_CLICK,
  185. onExceed: noop,
  186. onClear: noop,
  187. onDropdownVisibleChange: noop,
  188. onListScroll: noop,
  189. enableLeafClick: false,
  190. };
  191. options: any;
  192. isEmpty: boolean;
  193. inputRef: React.RefObject<typeof Input>;
  194. triggerRef: React.RefObject<HTMLDivElement>;
  195. optionsRef: React.RefObject<any>;
  196. clickOutsideHandler: any;
  197. mergeType: string;
  198. constructor(props: CascaderProps) {
  199. super(props);
  200. this.state = {
  201. disabledKeys: new Set(),
  202. isOpen: props.defaultOpen,
  203. /* By changing rePosKey, the dropdown position can be refreshed */
  204. rePosKey: resetkey,
  205. /* A data structure for storing cascader data items */
  206. keyEntities: {},
  207. /* Selected and show tick icon */
  208. selectedKeys: new Set([]),
  209. /* The key of the activated node */
  210. activeKeys: new Set([]),
  211. /* The key of the filtered node */
  212. filteredKeys: new Set([]),
  213. /* Value of input box */
  214. inputValue: '',
  215. /* Is searching */
  216. isSearching: false,
  217. /* The placeholder of input box */
  218. inputPlaceHolder: props.searchPlaceholder || props.placeholder,
  219. /* Cache props */
  220. prevProps: {},
  221. /* Is hovering */
  222. isHovering: false,
  223. /* Key of checked node, when multiple */
  224. checkedKeys: new Set([]),
  225. /* Key of half checked node, when multiple */
  226. halfCheckedKeys: new Set([]),
  227. /* Auto merged checkedKeys or leaf checkedKeys, when multiple */
  228. resolvedCheckedKeys: new Set([]),
  229. /* Keys of loaded item */
  230. loadedKeys: new Set(),
  231. /* Keys of loading item */
  232. loadingKeys: new Set(),
  233. /* Mark whether this rendering has triggered asynchronous loading of data */
  234. loading: false
  235. };
  236. this.options = {};
  237. this.isEmpty = false;
  238. this.mergeType = calcMergeType(props.autoMergeValue, props.leafOnly);
  239. this.inputRef = React.createRef();
  240. this.triggerRef = React.createRef();
  241. this.optionsRef = React.createRef();
  242. this.clickOutsideHandler = null;
  243. this.foundation = new CascaderFoundation(this.adapter);
  244. }
  245. get adapter(): CascaderAdapter {
  246. const filterAdapter: Pick<CascaderAdapter, 'updateInputValue' | 'updateInputPlaceHolder' | 'focusInput'> = {
  247. updateInputValue: value => {
  248. this.setState({ inputValue: value });
  249. },
  250. updateInputPlaceHolder: value => {
  251. this.setState({ inputPlaceHolder: value });
  252. },
  253. focusInput: () => {
  254. if (this.inputRef && this.inputRef.current) {
  255. // TODO: check the reason
  256. (this.inputRef.current as any).focus();
  257. }
  258. },
  259. };
  260. const cascaderAdapter: Pick<CascaderAdapter,
  261. 'registerClickOutsideHandler'
  262. | 'unregisterClickOutsideHandler'
  263. | 'rePositionDropdown'
  264. > = {
  265. registerClickOutsideHandler: cb => {
  266. const clickOutsideHandler = (e: Event) => {
  267. const optionInstance = this.optionsRef && this.optionsRef.current;
  268. const triggerDom = this.triggerRef && this.triggerRef.current;
  269. const optionsDom = ReactDOM.findDOMNode(optionInstance);
  270. const target = e.target as Element;
  271. if (
  272. optionsDom &&
  273. (!optionsDom.contains(target) || !optionsDom.contains(target.parentNode)) &&
  274. triggerDom &&
  275. !triggerDom.contains(target)
  276. ) {
  277. cb(e);
  278. }
  279. };
  280. this.clickOutsideHandler = clickOutsideHandler;
  281. document.addEventListener('mousedown', clickOutsideHandler, false);
  282. },
  283. unregisterClickOutsideHandler: () => {
  284. document.removeEventListener('mousedown', this.clickOutsideHandler, false);
  285. },
  286. rePositionDropdown: () => {
  287. let { rePosKey } = this.state;
  288. rePosKey = rePosKey + 1;
  289. this.setState({ rePosKey });
  290. },
  291. };
  292. return {
  293. ...super.adapter,
  294. ...filterAdapter,
  295. ...cascaderAdapter,
  296. updateStates: states => {
  297. this.setState({ ...states } as CascaderState);
  298. },
  299. openMenu: () => {
  300. this.setState({ isOpen: true });
  301. },
  302. closeMenu: cb => {
  303. this.setState({ isOpen: false }, () => {
  304. cb && cb();
  305. });
  306. },
  307. updateSelection: selectedKeys => this.setState({ selectedKeys }),
  308. notifyChange: value => {
  309. this.props.onChange && this.props.onChange(value);
  310. },
  311. notifySelect: selected => {
  312. this.props.onSelect && this.props.onSelect(selected);
  313. },
  314. notifyOnSearch: input => {
  315. this.props.onSearch && this.props.onSearch(input);
  316. },
  317. notifyFocus: (...v) => {
  318. this.props.onFocus && this.props.onFocus(...v);
  319. },
  320. notifyBlur: (...v) => {
  321. this.props.onBlur && this.props.onBlur(...v);
  322. },
  323. notifyDropdownVisibleChange: visible => {
  324. this.props.onDropdownVisibleChange(visible);
  325. },
  326. toggleHovering: bool => {
  327. this.setState({ isHovering: bool });
  328. },
  329. notifyLoadData: (selectedOpt, callback) => {
  330. const { loadData } = this.props;
  331. if (loadData) {
  332. new Promise<void>(resolve => {
  333. loadData(selectedOpt).then(() => {
  334. callback();
  335. this.setState({ loading: false });
  336. resolve();
  337. });
  338. });
  339. }
  340. },
  341. notifyOnLoad: (newLoadedKeys, data) => {
  342. const { onLoad } = this.props;
  343. onLoad && onLoad(newLoadedKeys, data);
  344. },
  345. notifyListScroll: (e, { panelIndex, activeNode }) => {
  346. this.props.onListScroll(e, { panelIndex, activeNode });
  347. },
  348. notifyOnExceed: data => this.props.onExceed(data),
  349. notifyClear: () => this.props.onClear(),
  350. };
  351. }
  352. static getDerivedStateFromProps(props: CascaderProps, prevState: CascaderState) {
  353. const {
  354. multiple,
  355. value,
  356. defaultValue,
  357. onChangeWithObject,
  358. leafOnly,
  359. autoMergeValue,
  360. } = props;
  361. const { prevProps } = prevState;
  362. let keyEntities = prevState.keyEntities || {};
  363. const newState: Partial<CascaderState> = { };
  364. const needUpdate = (name: string) => {
  365. const firstInProps = isEmpty(prevProps) && name in props;
  366. const nameHasChange = prevProps && !isEqual(prevProps[name], props[name]);
  367. return firstInProps || nameHasChange;
  368. };
  369. const needUpdateData = () => {
  370. const firstInProps = !prevProps && 'treeData' in props;
  371. const treeDataHasChange = prevProps && prevProps.treeData !== props.treeData;
  372. return firstInProps || treeDataHasChange;
  373. };
  374. const needUpdateTreeData = needUpdate('treeData') || needUpdateData();
  375. const needUpdateValue = needUpdate('value') || (isEmpty(prevProps) && defaultValue);
  376. if (multiple) {
  377. // when value and treedata need updated
  378. if (needUpdateTreeData || needUpdateValue) {
  379. // update state.keyEntities
  380. if (needUpdateTreeData) {
  381. newState.treeData = props.treeData;
  382. keyEntities = convertDataToEntities(props.treeData);
  383. newState.keyEntities = keyEntities;
  384. }
  385. let realKeys: Array<string> | Set<string> = prevState.checkedKeys;
  386. // when data was updated
  387. if (needUpdateValue) {
  388. // normallizedValue is used to save the value in two-dimensional array format
  389. let normallizedValue: SimpleValueType[][] = [];
  390. const realValue = needUpdate('value') ? value : defaultValue;
  391. // eslint-disable-next-line max-depth
  392. if (Array.isArray(realValue)) {
  393. normallizedValue = Array.isArray(realValue[0]) ?
  394. realValue as SimpleValueType[][] :
  395. [realValue] as SimpleValueType[][];
  396. } else {
  397. normallizedValue = [[realValue]];
  398. }
  399. // formatValuePath is used to save value of valuePath
  400. const formatValuePath: (string | number)[][] = [];
  401. normallizedValue.forEach((valueItem: SimpleValueType[]) => {
  402. const formatItem: (string | number)[] = onChangeWithObject ?
  403. (valueItem as CascaderData[]).map(i => i.value) :
  404. valueItem as (string | number)[];
  405. formatValuePath.push(formatItem);
  406. });
  407. // formatKeys is used to save key of value
  408. const formatKeys: any[] = [];
  409. formatValuePath.forEach(v => {
  410. const formatKeyItem = findKeysForValues(v, keyEntities);
  411. !isEmpty(formatKeyItem) && formatKeys.push(formatKeyItem);
  412. });
  413. realKeys = formatKeys;
  414. }
  415. if (isSet(realKeys)) {
  416. realKeys = [...realKeys];
  417. }
  418. const calRes = calcCheckedKeys(flatten(realKeys), keyEntities);
  419. const checkedKeys = new Set(calRes.checkedKeys);
  420. const halfCheckedKeys = new Set(calRes.halfCheckedKeys);
  421. // disableStrictly
  422. if (props.disableStrictly) {
  423. newState.disabledKeys = calcDisabledKeys(keyEntities);
  424. }
  425. const isLeafOnlyMerge = calcMergeType(autoMergeValue, leafOnly) === strings.LEAF_ONLY_MERGE_TYPE;
  426. newState.prevProps = props;
  427. newState.checkedKeys = checkedKeys;
  428. newState.halfCheckedKeys = halfCheckedKeys;
  429. newState.resolvedCheckedKeys = new Set(normalizeKeyList(checkedKeys, keyEntities, isLeafOnlyMerge));
  430. }
  431. }
  432. return newState;
  433. }
  434. componentDidMount() {
  435. this.foundation.init();
  436. }
  437. componentWillUnmount() {
  438. this.foundation.destroy();
  439. }
  440. componentDidUpdate(prevProps: CascaderProps) {
  441. let isOptionsChanged = false;
  442. if (!isEqual(prevProps.treeData, this.props.treeData)) {
  443. isOptionsChanged = true;
  444. this.foundation.collectOptions();
  445. }
  446. if (prevProps.value !== this.props.value && !isOptionsChanged) {
  447. this.foundation.handleValueChange(this.props.value);
  448. }
  449. }
  450. handleInputChange = (value: string) => {
  451. this.foundation.handleInputChange(value);
  452. };
  453. handleTagRemove = (e: any, tagValuePath: Array<string | number>) => {
  454. this.foundation.handleTagRemove(e, tagValuePath);
  455. };
  456. renderTagItem = (value: string | Array<string>, idx: number, type: string) => {
  457. const { keyEntities, disabledKeys } = this.state;
  458. const { size, disabled, displayProp, displayRender, disableStrictly } = this.props;
  459. const nodeKey = type === strings.IS_VALUE ?
  460. findKeysForValues(value, keyEntities)[0] :
  461. value;
  462. const isDsiabled = disabled ||
  463. keyEntities[nodeKey].data.disabled ||
  464. (disableStrictly && disabledKeys.has(nodeKey));
  465. if (!isEmpty(keyEntities) && !isEmpty(keyEntities[nodeKey])) {
  466. const tagCls = cls(`${prefixcls}-selection-tag`, {
  467. [`${prefixcls}-selection-tag-disabled`]: isDsiabled,
  468. });
  469. // custom render tags
  470. if (isFunction(displayRender)) {
  471. return displayRender(keyEntities[nodeKey], idx);
  472. // default render tags
  473. } else {
  474. return (
  475. <Tag
  476. size={size === 'default' ? 'large' : size}
  477. key={`tag-${nodeKey}-${idx}`}
  478. color="white"
  479. className={tagCls}
  480. closable
  481. onClose={(tagChildren, e) => {
  482. // When value has not changed, prevent clicking tag closeBtn to close tag
  483. e.preventDefault();
  484. this.handleTagRemove(e, keyEntities[nodeKey].valuePath);
  485. }}
  486. >
  487. {displayProp === 'label' && keyEntities[nodeKey].data.label}
  488. {displayProp === 'value' && keyEntities[nodeKey].data.value}
  489. </Tag>
  490. );
  491. }
  492. }
  493. return null;
  494. };
  495. renderTagInput() {
  496. const {
  497. size,
  498. disabled,
  499. placeholder,
  500. maxTagCount,
  501. showRestTagsPopover,
  502. restTagsPopoverProps,
  503. } = this.props;
  504. const {
  505. inputValue,
  506. checkedKeys,
  507. keyEntities,
  508. resolvedCheckedKeys
  509. } = this.state;
  510. const tagInputcls = cls(`${prefixcls}-tagInput-wrapper`);
  511. const tagValue: Array<Array<string>> = [];
  512. const realKeys = this.mergeType === strings.NONE_MERGE_TYPE
  513. ? checkedKeys
  514. : resolvedCheckedKeys;
  515. [...realKeys].forEach(checkedKey => {
  516. if (!isEmpty(keyEntities[checkedKey])) {
  517. tagValue.push(keyEntities[checkedKey].valuePath);
  518. }
  519. });
  520. return (
  521. <TagInput
  522. className={tagInputcls}
  523. ref={this.inputRef as any}
  524. disabled={disabled}
  525. size={size}
  526. // TODO Modify logic, not modify type
  527. value={tagValue as unknown as string[]}
  528. showRestTagsPopover={showRestTagsPopover}
  529. restTagsPopoverProps={restTagsPopoverProps}
  530. maxTagCount={maxTagCount}
  531. renderTagItem={(value, index) => this.renderTagItem(value, index, strings.IS_VALUE)}
  532. inputValue={inputValue}
  533. onInputChange={this.handleInputChange}
  534. // TODO Modify logic, not modify type
  535. onRemove={v => this.handleTagRemove(null, v as unknown as (string | number)[])}
  536. placeholder={placeholder}
  537. />
  538. );
  539. }
  540. renderInput() {
  541. const { size, disabled } = this.props;
  542. const inputcls = cls(`${prefixcls}-input`);
  543. const { inputValue, inputPlaceHolder } = this.state;
  544. const inputProps = {
  545. disabled,
  546. value: inputValue,
  547. className: inputcls,
  548. onChange: this.handleInputChange,
  549. placeholder: inputPlaceHolder,
  550. };
  551. const wrappercls = cls({
  552. [`${prefixcls}-search-wrapper`]: true,
  553. });
  554. return (
  555. <div className={wrappercls}>
  556. <Input
  557. ref={this.inputRef as any}
  558. size={size}
  559. {...inputProps}
  560. />
  561. </div>
  562. );
  563. }
  564. handleItemClick = (e: MouseEvent, item: Entity | Data) => {
  565. this.foundation.handleItemClick(e, item);
  566. };
  567. handleItemHover = (e: MouseEvent, item: Entity) => {
  568. this.foundation.handleItemHover(e, item);
  569. };
  570. onItemCheckboxClick = (item: Entity | Data) => {
  571. this.foundation.onItemCheckboxClick(item);
  572. };
  573. handleListScroll = (e: React.UIEvent<HTMLUListElement, UIEvent>, ind: number) => {
  574. this.foundation.handleListScroll(e, ind);
  575. };
  576. renderContent = () => {
  577. const {
  578. inputValue,
  579. isSearching,
  580. activeKeys,
  581. selectedKeys,
  582. checkedKeys,
  583. halfCheckedKeys,
  584. loadedKeys,
  585. loadingKeys
  586. } = this.state;
  587. const {
  588. filterTreeNode,
  589. dropdownClassName,
  590. dropdownStyle,
  591. loadData,
  592. emptyContent,
  593. separator,
  594. topSlot,
  595. bottomSlot,
  596. showNext,
  597. multiple
  598. } = this.props;
  599. const searchable = Boolean(filterTreeNode) && isSearching;
  600. const popoverCls = cls(dropdownClassName, `${prefixcls}-popover`);
  601. const renderData = this.foundation.getRenderData();
  602. const content = (
  603. <div className={popoverCls} role="listbox" style={dropdownStyle}>
  604. {topSlot}
  605. <Item
  606. activeKeys={activeKeys}
  607. selectedKeys={selectedKeys}
  608. separator={separator}
  609. loadedKeys={loadedKeys}
  610. loadingKeys={loadingKeys}
  611. onItemClick={this.handleItemClick}
  612. onItemHover={this.handleItemHover}
  613. showNext={showNext}
  614. onItemCheckboxClick={this.onItemCheckboxClick}
  615. onListScroll={this.handleListScroll}
  616. searchable={searchable}
  617. keyword={inputValue}
  618. emptyContent={emptyContent}
  619. loadData={loadData}
  620. data={renderData}
  621. multiple={multiple}
  622. checkedKeys={checkedKeys}
  623. halfCheckedKeys={halfCheckedKeys}
  624. />
  625. {bottomSlot}
  626. </div>
  627. );
  628. return content;
  629. };
  630. renderPlusN = (hiddenTag: Array<ReactNode>) => {
  631. const { disabled, showRestTagsPopover, restTagsPopoverProps } = this.props;
  632. const plusNCls = cls(`${prefixcls}-selection-n`, {
  633. [`${prefixcls}-selection-n-disabled`]: disabled
  634. });
  635. const renderPlusNChildren = (
  636. <span className={plusNCls}>
  637. +{hiddenTag.length}
  638. </span>
  639. );
  640. return (
  641. showRestTagsPopover && !disabled ?
  642. (
  643. <Popover
  644. content={hiddenTag}
  645. showArrow
  646. trigger="hover"
  647. position="top"
  648. autoAdjustOverflow
  649. {...restTagsPopoverProps}
  650. >
  651. {renderPlusNChildren}
  652. </Popover>
  653. ) :
  654. renderPlusNChildren
  655. );
  656. };
  657. renderMultipleTags = () => {
  658. const { autoMergeValue, maxTagCount } = this.props;
  659. const { checkedKeys, resolvedCheckedKeys } = this.state;
  660. const realKeys = this.mergeType === strings.NONE_MERGE_TYPE
  661. ? checkedKeys
  662. : resolvedCheckedKeys;
  663. const displayTag: Array<ReactNode> = [];
  664. const hiddenTag: Array<ReactNode> = [];
  665. [...realKeys].forEach((checkedKey, idx) => {
  666. const notExceedMaxTagCount = !isNumber(maxTagCount) || maxTagCount >= idx + 1;
  667. const item = this.renderTagItem(checkedKey, idx, strings.IS_KEY);
  668. if (notExceedMaxTagCount) {
  669. displayTag.push(item);
  670. } else {
  671. hiddenTag.push(item);
  672. }
  673. });
  674. return (
  675. <>
  676. {displayTag}
  677. {!isEmpty(hiddenTag) && this.renderPlusN(hiddenTag)}
  678. </>
  679. );
  680. };
  681. renderSelectContent = () => {
  682. const { placeholder, filterTreeNode, multiple } = this.props;
  683. const { selectedKeys, checkedKeys } = this.state;
  684. const searchable = Boolean(filterTreeNode);
  685. if (!searchable) {
  686. if (multiple) {
  687. if (isEmpty(checkedKeys)) {
  688. return <span className={`${prefixcls}-selection-placeholder`}>{placeholder}</span>;
  689. }
  690. return this.renderMultipleTags();
  691. } else {
  692. const displayText = selectedKeys.size ?
  693. this.foundation.renderDisplayText([...selectedKeys][0]) :
  694. '';
  695. const spanCls = cls({
  696. [`${prefixcls}-selection-placeholder`]: !displayText || !displayText.length,
  697. });
  698. return <span className={spanCls}>{displayText ? displayText : placeholder}</span>;
  699. }
  700. }
  701. const input = multiple ? this.renderTagInput() : this.renderInput();
  702. return input;
  703. };
  704. renderSuffix = () => {
  705. const { suffix }: any = this.props;
  706. const suffixWrapperCls = cls({
  707. [`${prefixcls}-suffix`]: true,
  708. [`${prefixcls}-suffix-text`]: suffix && isString(suffix),
  709. [`${prefixcls}-suffix-icon`]: isSemiIcon(suffix),
  710. });
  711. return <div className={suffixWrapperCls}>{suffix}</div>;
  712. };
  713. renderPrefix = () => {
  714. const { prefix, insetLabel, insetLabelId } = this.props;
  715. const labelNode: any = prefix || insetLabel;
  716. const prefixWrapperCls = cls({
  717. [`${prefixcls}-prefix`]: true,
  718. // to be doublechecked
  719. [`${prefixcls}-inset-label`]: insetLabel,
  720. [`${prefixcls}-prefix-text`]: labelNode && isString(labelNode),
  721. [`${prefixcls}-prefix-icon`]: isSemiIcon(labelNode),
  722. });
  723. return <div className={prefixWrapperCls} id={insetLabelId}>{labelNode}</div>;
  724. };
  725. renderCustomTrigger = () => {
  726. const { disabled, triggerRender, multiple } = this.props;
  727. const { selectedKeys, inputValue, inputPlaceHolder, resolvedCheckedKeys, checkedKeys } = this.state;
  728. let realValue;
  729. if (multiple) {
  730. if (this.mergeType === strings.NONE_MERGE_TYPE) {
  731. realValue = checkedKeys;
  732. } else {
  733. realValue = resolvedCheckedKeys;
  734. }
  735. } else {
  736. realValue = [...selectedKeys][0];
  737. }
  738. return (
  739. <Trigger
  740. value={realValue}
  741. inputValue={inputValue}
  742. onChange={this.handleInputChange}
  743. onClear={this.handleClear}
  744. placeholder={inputPlaceHolder}
  745. disabled={disabled}
  746. triggerRender={triggerRender}
  747. componentName={'Cascader'}
  748. componentProps={{ ...this.props }}
  749. />
  750. );
  751. };
  752. handleMouseOver = () => {
  753. this.foundation.toggleHoverState(true);
  754. };
  755. handleMouseLeave = () => {
  756. this.foundation.toggleHoverState(false);
  757. };
  758. handleClear = (e: MouseEvent) => {
  759. e && e.stopPropagation();
  760. this.foundation.handleClear();
  761. };
  762. showClearBtn = () => {
  763. const { showClear, disabled, multiple } = this.props;
  764. const { selectedKeys, isOpen, isHovering, checkedKeys } = this.state;
  765. const hasValue = selectedKeys.size;
  766. const multipleWithHaveValue = multiple && checkedKeys.size;
  767. return showClear && (hasValue || multipleWithHaveValue) && !disabled && (isOpen || isHovering);
  768. };
  769. renderClearBtn = () => {
  770. const clearCls = cls(`${prefixcls}-clearbtn`);
  771. const allowClear = this.showClearBtn();
  772. if (allowClear) {
  773. return (
  774. <div className={clearCls} onClick={this.handleClear} role='button' tabIndex={0}>
  775. <IconClear />
  776. </div>
  777. );
  778. }
  779. return null;
  780. };
  781. renderArrow = () => {
  782. const { arrowIcon } = this.props;
  783. const showClearBtn = this.showClearBtn();
  784. if (showClearBtn) {
  785. return null;
  786. }
  787. return arrowIcon ? <div className={cls(`${prefixcls}-arrow`)}>{arrowIcon}</div> : null;
  788. };
  789. renderSelection = () => {
  790. const {
  791. disabled,
  792. multiple,
  793. filterTreeNode,
  794. style,
  795. size,
  796. className,
  797. validateStatus,
  798. prefix,
  799. suffix,
  800. insetLabel,
  801. triggerRender,
  802. showClear,
  803. id,
  804. } = this.props;
  805. const { isOpen, isFocus, isInput, checkedKeys } = this.state;
  806. const filterable = Boolean(filterTreeNode);
  807. const useCustomTrigger = typeof triggerRender === 'function';
  808. const classNames = useCustomTrigger ?
  809. cls(className) :
  810. cls(prefixcls, className, {
  811. [`${prefixcls}-focus`]: isFocus || (isOpen && !isInput),
  812. [`${prefixcls}-disabled`]: disabled,
  813. [`${prefixcls}-single`]: true,
  814. [`${prefixcls}-filterable`]: filterable,
  815. [`${prefixcls}-error`]: validateStatus === 'error',
  816. [`${prefixcls}-warning`]: validateStatus === 'warning',
  817. [`${prefixcls}-small`]: size === 'small',
  818. [`${prefixcls}-large`]: size === 'large',
  819. [`${prefixcls}-with-prefix`]: prefix || insetLabel,
  820. [`${prefixcls}-with-suffix`]: suffix,
  821. });
  822. const mouseEvent = showClear ?
  823. {
  824. onMouseEnter: () => this.handleMouseOver(),
  825. onMouseLeave: () => this.handleMouseLeave(),
  826. } :
  827. {};
  828. const sectionCls = cls(`${prefixcls}-selection`, {
  829. [`${prefixcls}-selection-multiple`]: multiple && !isEmpty(checkedKeys),
  830. });
  831. const inner = useCustomTrigger ?
  832. this.renderCustomTrigger() :
  833. [
  834. <Fragment key={'prefix'}>{prefix || insetLabel ? this.renderPrefix() : null}</Fragment>,
  835. <Fragment key={'selection'}>
  836. <div className={sectionCls}>{this.renderSelectContent()}</div>
  837. </Fragment>,
  838. <Fragment key={'clearbtn'}>{this.renderClearBtn()}</Fragment>,
  839. <Fragment key={'suffix'}>{suffix ? this.renderSuffix() : null}</Fragment>,
  840. <Fragment key={'arrow'}>{this.renderArrow()}</Fragment>,
  841. ];
  842. /**
  843. * Reasons for disabling the a11y eslint rule:
  844. * The following attributes(aria-controls,aria-expanded) will be automatically added by Tooltip, no need to declare here
  845. */
  846. return (
  847. <div
  848. className={classNames}
  849. style={style}
  850. ref={this.triggerRef}
  851. onClick={e => this.foundation.handleClick(e)}
  852. aria-invalid={this.props['aria-invalid']}
  853. aria-errormessage={this.props['aria-errormessage']}
  854. aria-label={this.props['aria-label']}
  855. aria-labelledby={this.props['aria-labelledby']}
  856. aria-describedby={this.props["aria-describedby"]}
  857. aria-required={this.props['aria-required']}
  858. id={id}
  859. {...mouseEvent}
  860. // eslint-disable-next-line jsx-a11y/role-has-required-aria-props
  861. role='combobox'
  862. tabIndex={0}
  863. >
  864. {inner}
  865. </div>
  866. );
  867. };
  868. render() {
  869. const {
  870. zIndex,
  871. getPopupContainer,
  872. autoAdjustOverflow,
  873. stopPropagation,
  874. mouseLeaveDelay,
  875. mouseEnterDelay,
  876. } = this.props;
  877. const { isOpen, rePosKey } = this.state;
  878. const { direction } = this.context;
  879. const content = this.renderContent();
  880. const selection = this.renderSelection();
  881. const pos = direction === 'rtl' ? 'bottomRight' : 'bottomLeft';
  882. const mergedMotion: Motion = this.foundation.getMergedMotion();
  883. return (
  884. <Popover
  885. getPopupContainer={getPopupContainer}
  886. zIndex={zIndex}
  887. motion={mergedMotion}
  888. ref={this.optionsRef}
  889. content={content}
  890. visible={isOpen}
  891. trigger="custom"
  892. rePosKey={rePosKey}
  893. position={pos}
  894. autoAdjustOverflow={autoAdjustOverflow}
  895. stopPropagation={stopPropagation}
  896. mouseLeaveDelay={mouseLeaveDelay}
  897. mouseEnterDelay={mouseEnterDelay}
  898. >
  899. {selection}
  900. </Popover>
  901. );
  902. }
  903. }
  904. export default Cascader;