index.tsx 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288
  1. import React, { Fragment } from 'react';
  2. import ReactDOM from 'react-dom';
  3. import cls from 'classnames';
  4. import PropTypes from 'prop-types';
  5. import { isEqual, isString, isEmpty, noop, get, isFunction } from 'lodash';
  6. import TreeSelectFoundation, {
  7. Size,
  8. BasicTriggerRenderProps,
  9. /* Corresponding props */
  10. BasicTreeSelectProps,
  11. /* Corresponding state */
  12. BasicTreeSelectInnerData,
  13. TreeSelectAdapter
  14. } from '@douyinfe/semi-foundation/treeSelect/foundation';
  15. import {
  16. convertDataToEntities,
  17. flattenTreeData,
  18. calcExpandedKeysForValues,
  19. calcMotionKeys,
  20. findKeysForValues,
  21. calcCheckedKeys,
  22. calcExpandedKeys,
  23. getValueOrKey,
  24. normalizeKeyList,
  25. calcDisabledKeys,
  26. normalizeValue
  27. } from '@douyinfe/semi-foundation/tree/treeUtil';
  28. import { cssClasses, strings } from '@douyinfe/semi-foundation/treeSelect/constants';
  29. import { numbers as popoverNumbers } from '@douyinfe/semi-foundation/popover/constants';
  30. import { FixedSizeList as VirtualList, ListItemKeySelector } from 'react-window';
  31. import '@douyinfe/semi-foundation/tree/tree.scss';
  32. import '@douyinfe/semi-foundation/treeSelect/treeSelect.scss';
  33. import BaseComponent, { ValidateStatus } from '../_base/baseComponent';
  34. import ConfigContext from '../configProvider/context';
  35. import TagGroup from '../tag/group';
  36. import Tag, { TagProps } from '../tag/index';
  37. import Input, { InputProps } from '../input/index';
  38. import Popover from '../popover/index';
  39. import AutoSizer from '../tree/autoSizer';
  40. import TreeContext from '../tree/treeContext';
  41. import TreeNode from '../tree/treeNode';
  42. import NodeList from '../tree/nodeList';
  43. import { cloneDeep } from '../tree/treeUtil';
  44. import LocaleConsumer from '../locale/localeConsumer';
  45. import { Locale } from '../locale/interface';
  46. import Trigger from '../trigger';
  47. import TagInput from '../tagInput';
  48. import { isSemiIcon } from '../_utils';
  49. import { OptionProps, TreeProps, TreeState, FlattenNode, TreeNodeData, TreeNodeProps } from '../tree/interface';
  50. import { Motion } from '../_base/base';
  51. import { IconChevronDown, IconClear, IconSearch } from '@douyinfe/semi-icons';
  52. export type ExpandAction = false | 'click' | 'doubleClick';
  53. export interface TriggerRenderProps extends Omit<BasicTriggerRenderProps, 'componentProps'> {
  54. [x: string]: any;
  55. componentProps: TreeSelectProps;
  56. value: TreeNodeData[];
  57. onClear: (e: React.MouseEvent) => void;
  58. }
  59. export interface OnChange {
  60. /* onChangeWithObject is false */
  61. (
  62. value: TreeNodeData['value'] | Array<TreeNodeData['value']>,
  63. node: TreeNodeData[] | TreeNodeData,
  64. e: React.MouseEvent
  65. ): void;
  66. /* onChangeWithObject is true */
  67. (node: TreeNodeData[] | TreeNodeData, e: React.MouseEvent): void;
  68. }
  69. export type RenderSelectedItemInSingle = (treeNode: TreeNodeData) => React.ReactNode;
  70. export type RenderSelectedItemInMultiple = (
  71. treeNode: TreeNodeData,
  72. otherProps: { index: number | string; onClose: (tagContent: any, e: React.MouseEvent) => void }
  73. ) => {
  74. isRenderInTag: boolean;
  75. content: React.ReactNode;
  76. };
  77. export type RenderSelectedItem = RenderSelectedItemInSingle | RenderSelectedItemInMultiple;
  78. export type OverrideCommonProps =
  79. 'renderFullLabel'
  80. | 'renderLabel'
  81. | 'defaultValue'
  82. | 'emptyContent'
  83. | 'filterTreeNode'
  84. | 'style'
  85. | 'treeData'
  86. | 'value'
  87. | 'onExpand';
  88. /**
  89. * Type definition description:
  90. * TreeSelectProps inherits some properties from BasicTreeSelectProps (from foundation) and TreeProps (from semi-ui-react).
  91. */
  92. // eslint-disable-next-line max-len
  93. export interface TreeSelectProps extends Omit<BasicTreeSelectProps, OverrideCommonProps | 'validateStatus' | 'searchRender'>, Pick<TreeProps, OverrideCommonProps>{
  94. motion?: Motion;
  95. mouseEnterDelay?: number;
  96. mouseLeaveDelay?: number;
  97. arrowIcon?: React.ReactNode;
  98. autoAdjustOverflow?: boolean;
  99. clickToHide?: boolean;
  100. defaultOpen?: boolean;
  101. dropdownClassName?: string;
  102. dropdownMatchSelectWidth?: boolean;
  103. dropdownStyle?: React.CSSProperties;
  104. insetLabel?: React.ReactNode;
  105. maxTagCount?: number;
  106. motionExpand?: boolean;
  107. optionListStyle?: React.CSSProperties;
  108. outerBottomSlot?: React.ReactNode;
  109. outerTopSlot?: React.ReactNode;
  110. placeholder?: string;
  111. prefix?: React.ReactNode;
  112. searchAutoFocus?: boolean;
  113. searchPlaceholder?: string;
  114. showSearchClear?: boolean;
  115. size?: Size;
  116. suffix?: React.ReactNode;
  117. treeNodeLabelProp?: string;
  118. validateStatus?: ValidateStatus;
  119. zIndex?: number;
  120. searchPosition?: string;
  121. stopPropagation?: boolean | string;
  122. searchRender?: boolean | ((inputProps: InputProps) => React.ReactNode);
  123. onSelect?: (selectedKeys: string, selected: boolean, selectedNode: TreeNodeData) => void;
  124. renderSelectedItem?: RenderSelectedItem;
  125. getPopupContainer?: () => HTMLElement;
  126. triggerRender?: (props?: TriggerRenderProps) => React.ReactNode;
  127. onBlur?: (e: React.MouseEvent) => void;
  128. onChange?: OnChange;
  129. onFocus?: (e: React.MouseEvent) => void;
  130. onVisibleChange?: (isVisible: boolean) => void;
  131. }
  132. export type OverrideCommonState =
  133. 'keyEntities'
  134. | 'treeData'
  135. | 'disabledKeys'
  136. | 'flattenNodes';
  137. // eslint-disable-next-line max-len
  138. export interface TreeSelectState extends Omit<BasicTreeSelectInnerData, OverrideCommonState | 'prevProps'>, Pick<TreeState, OverrideCommonState> {
  139. inputTriggerFocus: boolean;
  140. isOpen: boolean;
  141. isInput: boolean;
  142. rePosKey: number;
  143. dropdownMinWidth: null | number;
  144. isHovering: boolean;
  145. prevProps: TreeSelectProps;
  146. }
  147. const prefixcls = cssClasses.PREFIX;
  148. const prefixTree = cssClasses.PREFIXTREE;
  149. const key = 0;
  150. class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
  151. static contextType = ConfigContext;
  152. static propTypes = {
  153. loadedKeys: PropTypes.arrayOf(PropTypes.string),
  154. loadData: PropTypes.func,
  155. onLoad: PropTypes.func,
  156. arrowIcon: PropTypes.node,
  157. defaultOpen: PropTypes.bool,
  158. defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
  159. defaultExpandAll: PropTypes.bool,
  160. defaultExpandedKeys: PropTypes.array,
  161. expandAll: PropTypes.bool,
  162. disabled: PropTypes.bool,
  163. disableStrictly: PropTypes.bool,
  164. // Whether to turn on the input box filtering function, when it is a function, it represents a custom filtering function
  165. filterTreeNode: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
  166. multiple: PropTypes.bool,
  167. searchPlaceholder: PropTypes.string,
  168. searchAutoFocus: PropTypes.bool,
  169. virtualize: PropTypes.object,
  170. treeNodeFilterProp: PropTypes.string,
  171. onChange: PropTypes.func,
  172. onSearch: PropTypes.func,
  173. onSelect: PropTypes.func,
  174. onExpand: PropTypes.func,
  175. onChangeWithObject: PropTypes.bool,
  176. onBlur: PropTypes.func,
  177. onFocus: PropTypes.func,
  178. value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array, PropTypes.object]),
  179. expandedKeys: PropTypes.array,
  180. autoExpandParent: PropTypes.bool,
  181. showClear: PropTypes.bool,
  182. showSearchClear: PropTypes.bool,
  183. autoAdjustOverflow: PropTypes.bool,
  184. showFilteredOnly: PropTypes.bool,
  185. motionExpand: PropTypes.bool,
  186. emptyContent: PropTypes.node,
  187. leafOnly: PropTypes.bool,
  188. treeData: PropTypes.arrayOf(
  189. PropTypes.shape({
  190. key: PropTypes.string.isRequired,
  191. value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  192. label: PropTypes.any,
  193. })
  194. ),
  195. dropdownClassName: PropTypes.string,
  196. dropdownStyle: PropTypes.object,
  197. motion: PropTypes.oneOfType([PropTypes.bool, PropTypes.object, PropTypes.func]),
  198. placeholder: PropTypes.string,
  199. maxTagCount: PropTypes.number,
  200. size: PropTypes.oneOf<TreeSelectProps['size']>(strings.SIZE_SET),
  201. className: PropTypes.string,
  202. style: PropTypes.object,
  203. treeNodeLabelProp: PropTypes.string,
  204. suffix: PropTypes.node,
  205. prefix: PropTypes.node,
  206. insetLabel: PropTypes.node,
  207. zIndex: PropTypes.number,
  208. getPopupContainer: PropTypes.func,
  209. dropdownMatchSelectWidth: PropTypes.bool,
  210. validateStatus: PropTypes.oneOf(strings.STATUS),
  211. mouseEnterDelay: PropTypes.number,
  212. mouseLeaveDelay: PropTypes.number,
  213. triggerRender: PropTypes.func,
  214. stopPropagation: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
  215. outerBottomSlot: PropTypes.node,
  216. outerTopSlot: PropTypes.node,
  217. onVisibleChange: PropTypes.func,
  218. expandAction: PropTypes.oneOf(['click' as const, 'doubleClick' as const, false as const]),
  219. searchPosition: PropTypes.oneOf([strings.SEARCH_POSITION_DROPDOWN, strings.SEARCH_POSITION_TRIGGER]),
  220. clickToHide: PropTypes.bool,
  221. renderLabel: PropTypes.func,
  222. renderFullLabel: PropTypes.func,
  223. labelEllipsis: PropTypes.bool,
  224. optionListStyle: PropTypes.object,
  225. searchRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
  226. renderSelectedItem: PropTypes.func,
  227. };
  228. static defaultProps: Partial<TreeSelectProps> = {
  229. searchPosition: strings.SEARCH_POSITION_DROPDOWN,
  230. arrowIcon: <IconChevronDown />,
  231. autoExpandParent: false,
  232. autoAdjustOverflow: true,
  233. stopPropagation: true,
  234. motion: true,
  235. motionExpand: true,
  236. expandAll: false,
  237. zIndex: popoverNumbers.DEFAULT_Z_INDEX,
  238. disabled: false,
  239. disableStrictly: false,
  240. multiple: false,
  241. filterTreeNode: false,
  242. size: 'default' as const,
  243. treeNodeFilterProp: 'label' as const,
  244. onChangeWithObject: false,
  245. treeNodeLabelProp: 'label' as const,
  246. dropdownMatchSelectWidth: true,
  247. defaultOpen: false,
  248. showSearchClear: true,
  249. showClear: false,
  250. onVisibleChange: noop,
  251. expandAction: false,
  252. clickToHide: true,
  253. searchAutoFocus: false,
  254. };
  255. inputRef: React.RefObject<typeof Input>;
  256. tagInputRef: React.RefObject<TagInput>;
  257. triggerRef: React.RefObject<HTMLDivElement>;
  258. optionsRef: React.RefObject<any>;
  259. clickOutsideHandler: any;
  260. _flattenNodes: TreeState['flattenNodes'];
  261. onNodeClick: any;
  262. onNodeDoubleClick: any;
  263. onMotionEnd: any;
  264. constructor(props: TreeSelectProps) {
  265. super(props);
  266. this.state = {
  267. inputTriggerFocus: false,
  268. isOpen: false,
  269. isInput: false,
  270. rePosKey: key,
  271. dropdownMinWidth: null,
  272. inputValue: '',
  273. keyEntities: {},
  274. treeData: [],
  275. flattenNodes: [],
  276. selectedKeys: [],
  277. checkedKeys: new Set(),
  278. halfCheckedKeys: new Set(),
  279. disabledKeys: new Set(),
  280. motionKeys: new Set([]),
  281. motionType: 'hide',
  282. expandedKeys: new Set(props.expandedKeys),
  283. filteredKeys: new Set(),
  284. filteredExpandedKeys: new Set(),
  285. filteredShownKeys: new Set(),
  286. prevProps: null,
  287. isHovering: false,
  288. cachedKeyValuePairs: {},
  289. loadedKeys: new Set(),
  290. loadingKeys: new Set(),
  291. };
  292. this.inputRef = React.createRef();
  293. this.tagInputRef = React.createRef();
  294. this.triggerRef = React.createRef();
  295. this.optionsRef = React.createRef();
  296. this.clickOutsideHandler = null;
  297. this.foundation = new TreeSelectFoundation(this.adapter);
  298. }
  299. // eslint-disable-next-line max-lines-per-function
  300. static getDerivedStateFromProps(props: TreeSelectProps, prevState: TreeSelectState) {
  301. const { prevProps, rePosKey } = prevState;
  302. const needUpdate = (name: string) => (
  303. (!prevProps && name in props) ||
  304. (prevProps && !isEqual(prevProps[name], props[name]))
  305. );
  306. let treeData;
  307. const withObject = props.onChangeWithObject;
  308. let keyEntities = prevState.keyEntities || {};
  309. let valueEntities = prevState.cachedKeyValuePairs || {};
  310. const newState: Partial<TreeSelectState> = {
  311. prevProps: props,
  312. };
  313. // TreeNode
  314. if (needUpdate('treeData')) {
  315. treeData = props.treeData;
  316. newState.treeData = treeData;
  317. const entitiesMap = convertDataToEntities(treeData);
  318. newState.keyEntities = {
  319. ...entitiesMap.keyEntities,
  320. };
  321. keyEntities = newState.keyEntities;
  322. newState.cachedKeyValuePairs = { ...entitiesMap.valueEntities };
  323. valueEntities = newState.cachedKeyValuePairs;
  324. }
  325. // if treeData keys changes, we won't show animation
  326. if (
  327. treeData &&
  328. props.motion &&
  329. !isEqual(new Set(Object.keys(newState.keyEntities)), new Set(Object.keys(prevState.keyEntities)))
  330. ) {
  331. if (prevProps && props.motion) {
  332. newState.motionKeys = new Set([]);
  333. newState.motionType = null;
  334. }
  335. }
  336. const expandAllWhenDataChange = needUpdate('treeData') && props.expandAll;
  337. // expandedKeys
  338. if (needUpdate('expandedKeys') || (prevProps && needUpdate('autoExpandParent'))) {
  339. newState.expandedKeys = calcExpandedKeys(
  340. props.expandedKeys,
  341. keyEntities,
  342. props.autoExpandParent || !prevProps
  343. );
  344. // only show animation when treeData does not change
  345. if (prevProps && props.motion && !treeData) {
  346. const { motionKeys, motionType } = calcMotionKeys(
  347. prevState.expandedKeys,
  348. newState.expandedKeys,
  349. keyEntities
  350. );
  351. newState.motionKeys = new Set(motionKeys);
  352. newState.motionType = motionType;
  353. }
  354. } else if ((!prevProps && (props.defaultExpandAll || props.expandAll)) || expandAllWhenDataChange) {
  355. newState.expandedKeys = new Set(Object.keys(keyEntities));
  356. } else if (!prevProps && props.defaultExpandedKeys) {
  357. newState.expandedKeys = calcExpandedKeys(props.defaultExpandedKeys, keyEntities);
  358. } else if (!prevProps && props.defaultValue) {
  359. newState.expandedKeys = calcExpandedKeysForValues(
  360. normalizeValue(props.defaultValue, withObject),
  361. keyEntities,
  362. props.multiple,
  363. valueEntities
  364. );
  365. } else if (!prevProps && props.value) {
  366. newState.expandedKeys = calcExpandedKeysForValues(
  367. normalizeValue(props.value, withObject),
  368. keyEntities,
  369. props.multiple,
  370. valueEntities
  371. );
  372. }
  373. // flattenNodes
  374. if (treeData || needUpdate('expandedKeys')) {
  375. const flattenNodes = flattenTreeData(
  376. treeData || prevState.treeData,
  377. newState.expandedKeys || prevState.expandedKeys
  378. );
  379. newState.flattenNodes = flattenNodes;
  380. }
  381. // selectedKeys: single mode controlled
  382. const isMultiple = props.multiple;
  383. if (!isMultiple) {
  384. if (needUpdate('value')) {
  385. newState.selectedKeys = findKeysForValues(
  386. normalizeValue(props.value, withObject),
  387. valueEntities,
  388. isMultiple
  389. );
  390. } else if (!prevProps && props.defaultValue) {
  391. newState.selectedKeys = findKeysForValues(
  392. normalizeValue(props.defaultValue, withObject),
  393. valueEntities,
  394. isMultiple
  395. );
  396. } else if (treeData) {
  397. // If `treeData` changed, we also need check it
  398. newState.selectedKeys = findKeysForValues(
  399. normalizeValue(props.value, withObject) || '',
  400. valueEntities,
  401. isMultiple
  402. );
  403. }
  404. } else {
  405. // checkedKeys: multiple mode controlled || data changed
  406. let checkedKeyValues;
  407. if (needUpdate('value')) {
  408. checkedKeyValues = findKeysForValues(
  409. normalizeValue(props.value, withObject),
  410. valueEntities,
  411. isMultiple
  412. );
  413. } else if (!prevProps && props.defaultValue) {
  414. checkedKeyValues = findKeysForValues(
  415. normalizeValue(props.defaultValue, withObject),
  416. valueEntities,
  417. isMultiple
  418. );
  419. } else if (treeData) {
  420. // If `treeData` changed, we also need check it
  421. checkedKeyValues = findKeysForValues(
  422. normalizeValue(props.value, withObject) || [],
  423. valueEntities,
  424. isMultiple
  425. );
  426. }
  427. if (checkedKeyValues) {
  428. const { checkedKeys, halfCheckedKeys } = calcCheckedKeys(checkedKeyValues, keyEntities);
  429. newState.checkedKeys = checkedKeys;
  430. newState.halfCheckedKeys = halfCheckedKeys;
  431. }
  432. }
  433. // loadedKeys
  434. if (needUpdate('loadedKeys')) {
  435. newState.loadedKeys = new Set(props.loadedKeys);
  436. }
  437. // ================== rePosKey ==================
  438. if (needUpdate('treeData') || needUpdate('value')) {
  439. newState.rePosKey = rePosKey + 1;
  440. }
  441. // ================ disableStrictly =================
  442. if (treeData && props.disableStrictly) {
  443. newState.disabledKeys = calcDisabledKeys(keyEntities);
  444. }
  445. return newState;
  446. }
  447. get adapter(): TreeSelectAdapter<TreeSelectProps, TreeSelectState> {
  448. const filterAdapter: Pick<TreeSelectAdapter, 'updateInputValue'> = {
  449. updateInputValue: value => {
  450. this.setState({ inputValue: value });
  451. }
  452. };
  453. const treeSelectAdapter: Pick<TreeSelectAdapter,
  454. 'registerClickOutsideHandler'
  455. | 'unregisterClickOutsideHandler'
  456. | 'rePositionDropdown'
  457. > = {
  458. registerClickOutsideHandler: cb => {
  459. const clickOutsideHandler = (e: Event) => {
  460. const optionInstance = this.optionsRef && this.optionsRef.current as React.ReactInstance;
  461. const triggerDom = this.triggerRef && this.triggerRef.current;
  462. // eslint-disable-next-line
  463. const optionsDom = ReactDOM.findDOMNode(optionInstance);
  464. const target = e.target as Element;
  465. if (
  466. optionsDom &&
  467. (
  468. !optionsDom.contains(target) ||
  469. !optionsDom.contains(target.parentNode)
  470. ) &&
  471. triggerDom &&
  472. !triggerDom.contains(target)
  473. ) {
  474. cb(e);
  475. }
  476. };
  477. this.clickOutsideHandler = clickOutsideHandler;
  478. document.addEventListener('mousedown', clickOutsideHandler, false);
  479. },
  480. unregisterClickOutsideHandler: () => {
  481. document.removeEventListener('mousedown', this.clickOutsideHandler, false);
  482. this.clickOutsideHandler = null;
  483. },
  484. rePositionDropdown: () => {
  485. let { rePosKey } = this.state;
  486. rePosKey = rePosKey + 1;
  487. this.setState({ rePosKey });
  488. },
  489. };
  490. const treeAdapter: Pick<TreeSelectAdapter,
  491. 'updateState'
  492. | 'notifySelect'
  493. | 'notifySearch'
  494. | 'cacheFlattenNodes'
  495. | 'notifyLoad'
  496. > = {
  497. updateState: states => {
  498. this.setState({ ...states } as TreeSelectState);
  499. },
  500. notifySelect: ((selectKey, bool, node) => {
  501. this.props.onSelect && this.props.onSelect(selectKey, bool, node);
  502. }),
  503. notifySearch: input => {
  504. this.props.onSearch && this.props.onSearch(input);
  505. },
  506. cacheFlattenNodes: bool => {
  507. this._flattenNodes = bool ? cloneDeep(this.state.flattenNodes) : null;
  508. },
  509. notifyLoad: (newLoadedKeys, data) => {
  510. const { onLoad } = this.props;
  511. isFunction(onLoad) && onLoad(newLoadedKeys, data);
  512. }
  513. };
  514. return {
  515. ...super.adapter,
  516. ...filterAdapter,
  517. ...treeSelectAdapter,
  518. ...treeAdapter,
  519. updateLoadKeys: (data, resolve) => {
  520. this.setState(({ loadedKeys, loadingKeys }) =>
  521. this.foundation.handleNodeLoad(loadedKeys, loadingKeys, data, resolve));
  522. },
  523. updateState: states => {
  524. this.setState({ ...states });
  525. },
  526. openMenu: () => {
  527. this.setState({ isOpen: true }, () => {
  528. this.props.onVisibleChange(true);
  529. });
  530. },
  531. closeMenu: cb => {
  532. this.setState({ isOpen: false }, () => {
  533. cb && cb();
  534. this.props.onVisibleChange(false);
  535. });
  536. },
  537. getTriggerWidth: () => {
  538. const el = this.triggerRef.current;
  539. return el && el.getBoundingClientRect().width;
  540. },
  541. setOptionWrapperWidth: width => {
  542. this.setState({ dropdownMinWidth: width });
  543. },
  544. notifyChange: (value, node, e) => {
  545. this.props.onChange && this.props.onChange(value, node, e);
  546. },
  547. notifyChangeWithObject: (node, e) => {
  548. this.props.onChange && this.props.onChange(node, e);
  549. },
  550. notifyExpand: (expandedKeys, { expanded: bool, node }) => {
  551. this.props.onExpand && this.props.onExpand([...expandedKeys], { expanded: bool, node });
  552. if (bool && this.props.loadData) {
  553. this.onNodeLoad(node);
  554. }
  555. },
  556. notifyFocus: (...v) => {
  557. this.props.onFocus && this.props.onFocus(...v);
  558. },
  559. notifyBlur: (...v) => {
  560. this.props.onBlur && this.props.onBlur(...v);
  561. },
  562. toggleHovering: bool => {
  563. this.setState({ isHovering: bool });
  564. },
  565. updateInputFocus: bool => {} // eslint-disable-line
  566. };
  567. }
  568. componentDidMount() {
  569. this.foundation.init();
  570. }
  571. componentWillUnmount() {
  572. this.foundation.destroy();
  573. }
  574. renderSuffix = () => {
  575. const { suffix }: any = this.props;
  576. const suffixWrapperCls = cls({
  577. [`${prefixcls}-suffix`]: true,
  578. [`${prefixcls}-suffix-text`]: suffix && isString(suffix),
  579. [`${prefixcls}-suffix-icon`]: isSemiIcon(suffix),
  580. });
  581. return <div className={suffixWrapperCls}>{suffix}</div>;
  582. };
  583. renderPrefix = () => {
  584. const { prefix, insetLabel }: any = this.props;
  585. const labelNode = prefix || insetLabel;
  586. const prefixWrapperCls = cls({
  587. [`${prefixcls}-prefix`]: true,
  588. // to be doublechecked
  589. [`${prefixcls}-inset-label`]: insetLabel,
  590. [`${prefixcls}-prefix-text`]: labelNode && isString(labelNode),
  591. [`${prefixcls}-prefix-icon`]: isSemiIcon(labelNode),
  592. });
  593. return <div className={prefixWrapperCls}>{labelNode}</div>;
  594. };
  595. renderContent = () => {
  596. const { dropdownMinWidth } = this.state;
  597. const { dropdownStyle, dropdownClassName } = this.props;
  598. const style = { minWidth: dropdownMinWidth, ...dropdownStyle };
  599. const popoverCls = cls(dropdownClassName, `${prefixcls}-popover`);
  600. return (
  601. <div className={popoverCls} role="list-box" style={style}>
  602. {this.renderTree()}
  603. </div>
  604. );
  605. };
  606. removeTag = (removedKey: TreeNodeData['key']) => {
  607. this.foundation.removeTag(removedKey);
  608. };
  609. handleClick = (e: React.MouseEvent) => {
  610. this.foundation.handleClick(e);
  611. };
  612. showClearBtn = () => {
  613. const { searchPosition } = this.props;
  614. const { inputValue } = this.state;
  615. const triggerSearchHasInputValue = searchPosition === strings.SEARCH_POSITION_TRIGGER && inputValue;
  616. const { showClear, disabled, multiple } = this.props;
  617. const { selectedKeys, checkedKeys, isOpen, isHovering } = this.state;
  618. const hasValue = multiple ? Boolean(checkedKeys.size) : Boolean(selectedKeys.length);
  619. return showClear && (hasValue || triggerSearchHasInputValue) && !disabled && (isOpen || isHovering);
  620. };
  621. renderTagList = () => {
  622. const { checkedKeys, keyEntities, disabledKeys } = this.state;
  623. const {
  624. treeNodeLabelProp,
  625. leafOnly,
  626. disabled,
  627. disableStrictly,
  628. size,
  629. renderSelectedItem: propRenderSelectedItem
  630. } = this.props;
  631. const renderSelectedItem = isFunction(propRenderSelectedItem) ?
  632. propRenderSelectedItem :
  633. (item: TreeNodeData) => ({
  634. isRenderInTag: true,
  635. content: get(item, treeNodeLabelProp, null)
  636. });
  637. const renderKeys = normalizeKeyList([...checkedKeys], keyEntities, leafOnly);
  638. const tagList: Array<React.ReactNode> = [];
  639. // eslint-disable-next-line @typescript-eslint/no-shadow
  640. renderKeys.forEach((key: TreeNodeData['key']) => {
  641. const item = keyEntities[key].data;
  642. const onClose = (tagContent: any, e: React.MouseEvent) => {
  643. if (e && typeof e.preventDefault === 'function') {
  644. // make sure that tag will not hidden immediately in controlled mode
  645. e.preventDefault();
  646. }
  647. this.removeTag(key);
  648. };
  649. const { content, isRenderInTag } = (treeNodeLabelProp in item && item) ?
  650. (renderSelectedItem as RenderSelectedItemInMultiple)(item, { index: key, onClose }) :
  651. null;
  652. if (!content) {
  653. return;
  654. }
  655. const isDisabled = disabled || item.disabled || (disableStrictly && disabledKeys.has(item.key));
  656. const tag: Partial<TagProps> & React.Attributes = {
  657. closable: !isDisabled,
  658. color: 'white',
  659. visible: true,
  660. onClose,
  661. key,
  662. size: size === 'small' ? 'small' : 'large'
  663. };
  664. if (isRenderInTag) {
  665. // pass ReactNode list to tagList when using tagGroup custom mode
  666. tagList.push(<Tag {...tag}>{content}</Tag>);
  667. } else {
  668. tagList.push(content);
  669. }
  670. });
  671. return tagList;
  672. };
  673. /**
  674. * When single selection and the search box is on trigger, the items displayed in the rendered search box
  675. */
  676. renderSingleTriggerSearchItem = () => {
  677. const { placeholder, disabled } = this.props;
  678. const { inputTriggerFocus } = this.state;
  679. const renderText = this.foundation.getRenderTextInSingle();
  680. const spanCls = cls(`${prefixcls}-selection-TriggerSearchItem`, {
  681. [`${prefixcls}-selection-TriggerSearchItem-placeholder`]: (inputTriggerFocus || !renderText) && !disabled,
  682. [`${prefixcls}-selection-TriggerSearchItem-disabled`]: disabled,
  683. });
  684. return (
  685. <span className={spanCls}>
  686. {renderText ? renderText : placeholder}
  687. </span>
  688. );
  689. };
  690. /**
  691. * Single selection and the search box content rendered when the search box is on trigger
  692. */
  693. renderSingleTriggerSearch = () => {
  694. const { inputValue } = this.state;
  695. return (
  696. <>
  697. {!inputValue && this.renderSingleTriggerSearchItem()}
  698. {this.renderInput()}
  699. </>
  700. );
  701. };
  702. renderSelectContent = () => {
  703. const {
  704. multiple,
  705. placeholder,
  706. maxTagCount,
  707. searchPosition,
  708. filterTreeNode,
  709. } = this.props;
  710. const { selectedKeys, checkedKeys } = this.state;
  711. const hasValue = multiple ? Boolean(checkedKeys.size) : Boolean(selectedKeys.length);
  712. const isTriggerPositionSearch = filterTreeNode && searchPosition === strings.SEARCH_POSITION_TRIGGER;
  713. // searchPosition = trigger
  714. if (isTriggerPositionSearch) {
  715. return multiple ? this.renderTagInput() : this.renderSingleTriggerSearch();
  716. }
  717. // searchPosition = dropdown and single seleciton
  718. if (!multiple || !hasValue) {
  719. const renderText = this.foundation.getRenderTextInSingle();
  720. const spanCls = cls({
  721. [`${prefixcls}-selection-placeholder`]: !renderText,
  722. });
  723. return <span className={spanCls}>{renderText ? renderText : placeholder}</span>;
  724. }
  725. // searchPosition = dropdown and multiple seleciton
  726. const tagList = this.renderTagList();
  727. // mode=custom to return tagList directly
  728. return (
  729. <TagGroup
  730. maxTagCount={maxTagCount}
  731. tagList={tagList}
  732. size="large"
  733. mode="custom"
  734. />
  735. );
  736. };
  737. handleClear = (e: React.MouseEvent) => {
  738. e && e.stopPropagation();
  739. this.foundation.handleClear(e);
  740. };
  741. handleMouseOver = (e: React.MouseEvent) => {
  742. this.foundation.toggleHoverState(true);
  743. };
  744. handleMouseLeave = (e: React.MouseEvent) => {
  745. this.foundation.toggleHoverState(false);
  746. };
  747. search = (value: string) => {
  748. this.foundation.handleInputChange(value);
  749. };
  750. close = () => {
  751. this.foundation.close(null);
  752. };
  753. renderArrow = () => {
  754. const showClearBtn = this.showClearBtn();
  755. const { arrowIcon } = this.props;
  756. if (showClearBtn) {
  757. return null;
  758. }
  759. return arrowIcon ? <div className={cls(`${prefixcls}-arrow`)}>{arrowIcon}</div> : null;
  760. };
  761. renderClearBtn = () => {
  762. const showClearBtn = this.showClearBtn();
  763. const clearCls = cls(`${prefixcls}-clearbtn`);
  764. if (showClearBtn) {
  765. return (
  766. <div className={clearCls} onClick={this.handleClear}>
  767. <IconClear />
  768. </div>
  769. );
  770. }
  771. return null;
  772. };
  773. renderSelection = () => {
  774. const {
  775. disabled,
  776. multiple,
  777. filterTreeNode,
  778. validateStatus,
  779. prefix,
  780. suffix,
  781. style,
  782. size,
  783. insetLabel,
  784. className,
  785. placeholder,
  786. showClear,
  787. leafOnly,
  788. searchPosition,
  789. triggerRender,
  790. } = this.props;
  791. const { isOpen, isInput, inputValue, selectedKeys, checkedKeys, keyEntities } = this.state;
  792. const filterable = Boolean(filterTreeNode);
  793. const useCustomTrigger = typeof triggerRender === 'function';
  794. const mouseEvent = showClear ?
  795. {
  796. onMouseEnter: (e: React.MouseEvent) => this.handleMouseOver(e),
  797. onMouseLeave: (e: React.MouseEvent) => this.handleMouseLeave(e),
  798. } :
  799. {};
  800. const isTriggerPositionSearch = searchPosition === strings.SEARCH_POSITION_TRIGGER && filterable;
  801. const isEmptyTriggerSearch = isTriggerPositionSearch && isEmpty(checkedKeys);
  802. const isValueTriggerSearch = isTriggerPositionSearch && !isEmpty(checkedKeys);
  803. const classNames = useCustomTrigger ?
  804. cls(className) :
  805. cls(
  806. prefixcls,
  807. {
  808. [`${prefixcls}-focus`]: isOpen && !isInput,
  809. [`${prefixcls}-disabled`]: disabled,
  810. [`${prefixcls}-single`]: !multiple,
  811. [`${prefixcls}-multiple`]: multiple,
  812. [`${prefixcls}-multiple-tagInput-empty`]: multiple && isEmptyTriggerSearch,
  813. [`${prefixcls}-multiple-tagInput-notEmpty`]: multiple && isValueTriggerSearch,
  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. [`${prefixcls}-with-suffix`]: suffix,
  822. },
  823. className
  824. );
  825. const triggerRenderKeys = multiple ? normalizeKeyList([...checkedKeys], keyEntities, leafOnly) : selectedKeys;
  826. const inner = useCustomTrigger ? (
  827. <Trigger
  828. inputValue={inputValue}
  829. // eslint-disable-next-line @typescript-eslint/no-shadow
  830. value={triggerRenderKeys.map((key: string) => get(keyEntities, [key, 'data']))}
  831. disabled={disabled}
  832. placeholder={placeholder}
  833. onClear={this.handleClear}
  834. componentName={'TreeSelect'}
  835. triggerRender={triggerRender}
  836. componentProps={{ ...this.props }}
  837. />
  838. ) : (
  839. [
  840. <Fragment key={'prefix'}>{prefix || insetLabel ? this.renderPrefix() : null}</Fragment>,
  841. <Fragment key={'selection'}>
  842. <div className={`${prefixcls}-selection`}>{this.renderSelectContent()}</div>
  843. </Fragment>,
  844. <Fragment key={'suffix'}>{suffix ? this.renderSuffix() : null}</Fragment>,
  845. <Fragment key={'clearBtn'}>
  846. {
  847. (showClear || (isTriggerPositionSearch && inputValue)) ?
  848. this.renderClearBtn() :
  849. null
  850. }
  851. </Fragment>,
  852. <Fragment key={'arrow'}>{this.renderArrow()}</Fragment>,
  853. ]
  854. );
  855. return (
  856. <div
  857. className={classNames}
  858. style={style}
  859. ref={this.triggerRef}
  860. onClick={this.handleClick}
  861. {...mouseEvent}
  862. >
  863. {inner}
  864. </div>
  865. );
  866. };
  867. // eslint-disable-next-line @typescript-eslint/no-shadow
  868. renderTagItem = (key: string, idx: number) => {
  869. const { keyEntities, disabledKeys } = this.state;
  870. const {
  871. size,
  872. leafOnly,
  873. disabled,
  874. disableStrictly,
  875. renderSelectedItem: propRenderSelectedItem,
  876. treeNodeLabelProp
  877. } = this.props;
  878. const keyList = normalizeKeyList([key], keyEntities, leafOnly);
  879. const nodes = keyList.map(i => keyEntities[i].data);
  880. const value = getValueOrKey(nodes);
  881. const tagCls = cls(`${prefixcls}-selection-tag`, {
  882. [`${prefixcls}-selection-tag-disabled`]: disabled,
  883. });
  884. const nodeHaveData = !isEmpty(nodes) && !isEmpty(nodes[0]);
  885. const isDisableStrictlyNode = disableStrictly && nodeHaveData && disabledKeys.has(nodes[0].key);
  886. const closable = nodeHaveData && !nodes[0].disabled && !disabled && !isDisableStrictlyNode;
  887. const onClose = (tagChildren: React.ReactNode, e: React.MouseEvent) => {
  888. // When value has not changed, prevent clicking tag closeBtn to close tag
  889. e.preventDefault();
  890. this.removeTag(key);
  891. };
  892. const tagProps: Partial<TagProps> & React.Attributes = {
  893. size: size === 'small' ? 'small' : 'large',
  894. key: `tag-${value}-${idx}`,
  895. color: 'white',
  896. className: tagCls,
  897. closable,
  898. onClose,
  899. };
  900. const item = nodes[0];
  901. const renderSelectedItem = isFunction(propRenderSelectedItem) ? propRenderSelectedItem :
  902. (selectedItem: TreeNodeData) => ({
  903. isRenderInTag: true,
  904. content: get(selectedItem, treeNodeLabelProp, null)
  905. });
  906. if (isFunction(renderSelectedItem)) {
  907. const { content, isRenderInTag } = treeNodeLabelProp in item && item ?
  908. (renderSelectedItem as RenderSelectedItemInMultiple)(item, { index: idx, onClose }):
  909. null;
  910. if (isRenderInTag) {
  911. return <Tag {...tagProps}>{content}</Tag>;
  912. } else {
  913. return content;
  914. }
  915. }
  916. return (
  917. <Tag {...tagProps}>
  918. {value}
  919. </Tag>
  920. );
  921. };
  922. renderTagInput = () => {
  923. const {
  924. leafOnly,
  925. disabled,
  926. size,
  927. searchAutoFocus,
  928. placeholder,
  929. } = this.props;
  930. const {
  931. keyEntities,
  932. checkedKeys,
  933. inputValue
  934. } = this.state;
  935. const keyList = normalizeKeyList(checkedKeys, keyEntities, leafOnly);
  936. return (
  937. <TagInput
  938. disabled={disabled}
  939. onInputChange={v => this.search(v)}
  940. ref={this.tagInputRef}
  941. placeholder={placeholder}
  942. value={keyList}
  943. inputValue={inputValue}
  944. size={size}
  945. autoFocus={searchAutoFocus}
  946. renderTagItem={(itemKey, index) => this.renderTagItem(itemKey, index)}
  947. onRemove={itemKey => this.removeTag(itemKey)}
  948. />
  949. );
  950. };
  951. // render Tree
  952. renderInput = () => {
  953. const {
  954. searchPlaceholder,
  955. searchRender,
  956. showSearchClear,
  957. searchPosition,
  958. searchAutoFocus,
  959. multiple,
  960. disabled,
  961. } = this.props;
  962. const isDropdownPositionSearch = searchPosition === strings.SEARCH_POSITION_DROPDOWN;
  963. const inputcls = cls({
  964. [`${prefixTree}-input`]: isDropdownPositionSearch,
  965. [`${prefixcls}-inputTrigger`]: !isDropdownPositionSearch
  966. });
  967. const { inputValue } = this.state;
  968. const baseInputProps = {
  969. value: inputValue,
  970. className: inputcls,
  971. onChange: (value: string) => this.search(value),
  972. };
  973. const inputDropdownProps = {
  974. showClear: showSearchClear,
  975. prefix: <IconSearch />,
  976. };
  977. const inputTriggerProps = {
  978. onFocus: (e: React.FocusEvent) => this.foundation.handleInputTriggerFocus(),
  979. onBlur: (e: React.FocusEvent) => this.foundation.handleInputTriggerBlur(),
  980. disabled,
  981. };
  982. const realInputProps = isDropdownPositionSearch ? inputDropdownProps : inputTriggerProps;
  983. const wrapperCls = cls({
  984. [`${prefixTree}-search-wrapper`]: isDropdownPositionSearch,
  985. [`${prefixcls}-triggerSingleSearch-wrapper`]: !isDropdownPositionSearch && !multiple,
  986. });
  987. const useCusSearch = typeof searchRender === 'function' || typeof searchRender === 'boolean';
  988. if (useCusSearch && !searchRender) {
  989. return null;
  990. }
  991. return (
  992. <div className={wrapperCls}>
  993. <LocaleConsumer componentName="TreeSelect">
  994. {(locale: Locale['TreeSelect']) => {
  995. const placeholder = isDropdownPositionSearch ?
  996. searchPlaceholder || locale.searchPlaceholder :
  997. '';
  998. if (useCusSearch) {
  999. return (searchRender as any)({ ...realInputProps, ...baseInputProps, placeholder });
  1000. }
  1001. return (
  1002. <Input
  1003. ref={this.inputRef as any}
  1004. autofocus={searchAutoFocus}
  1005. placeholder={placeholder}
  1006. {...baseInputProps}
  1007. {...realInputProps}
  1008. />
  1009. );
  1010. }}
  1011. </LocaleConsumer>
  1012. </div>
  1013. );
  1014. };
  1015. renderEmpty = () => {
  1016. const { emptyContent } = this.props;
  1017. if (emptyContent) {
  1018. return <TreeNode empty emptyContent={this.props.emptyContent} />;
  1019. } else {
  1020. return (
  1021. <LocaleConsumer componentName="Tree">
  1022. {(locale: Locale['Tree']) => <TreeNode empty emptyContent={locale.emptyText} />}
  1023. </LocaleConsumer>
  1024. );
  1025. }
  1026. };
  1027. onNodeLoad = (data: TreeNodeData) => new Promise(resolve => this.foundation.setLoadKeys(data, resolve));
  1028. onNodeSelect = (e: React.MouseEvent, treeNode: TreeNodeProps) => {
  1029. this.foundation.handleNodeSelect(e, treeNode);
  1030. };
  1031. onNodeCheck = (e: React.MouseEvent, treeNode: TreeNodeProps) => {
  1032. this.foundation.handleNodeSelect(e, treeNode);
  1033. };
  1034. onNodeExpand = (e: React.MouseEvent, treeNode: TreeNodeProps) => {
  1035. this.foundation.handleNodeExpand(e, treeNode);
  1036. };
  1037. getTreeNodeRequiredProps = () => {
  1038. const { expandedKeys, selectedKeys, checkedKeys, halfCheckedKeys, keyEntities, filteredKeys } = this.state;
  1039. return {
  1040. expandedKeys: expandedKeys || new Set(),
  1041. selectedKeys: selectedKeys || [],
  1042. checkedKeys: checkedKeys || new Set(),
  1043. halfCheckedKeys: halfCheckedKeys || new Set(),
  1044. filteredKeys: filteredKeys || new Set(),
  1045. keyEntities,
  1046. };
  1047. };
  1048. getTreeNodeKey = (treeNode: TreeNodeData) => {
  1049. const { data } = treeNode;
  1050. // eslint-disable-next-line @typescript-eslint/no-shadow
  1051. const { key }: { key: string } = data;
  1052. return key;
  1053. };
  1054. /* Event handler function after popover is closed */
  1055. handlePopoverClose = isVisible => {
  1056. const { filterTreeNode } = this.props;
  1057. if (isVisible === false && Boolean(filterTreeNode)) {
  1058. this.foundation.clearInput();
  1059. }
  1060. }
  1061. renderTreeNode = (treeNode: FlattenNode, ind: number, style: React.CSSProperties) => {
  1062. const { data } = treeNode;
  1063. // eslint-disable-next-line @typescript-eslint/no-shadow
  1064. const { key }: { key: string } = data;
  1065. const treeNodeProps = this.foundation.getTreeNodeProps(key);
  1066. if (!treeNodeProps) {
  1067. return null;
  1068. }
  1069. return <TreeNode {...treeNodeProps} {...data} key={key} data={data} style={style} />;
  1070. };
  1071. itemKey = (index: number, data: TreeNodeData) => {
  1072. // Find the item at the specified index.
  1073. const item = data[index];
  1074. // Return a value that uniquely identifies this item.
  1075. return item.key;
  1076. };
  1077. renderNodeList = () => {
  1078. const { flattenNodes, motionKeys, motionType } = this.state;
  1079. const { direction } = this.context;
  1080. const { virtualize, motionExpand } = this.props;
  1081. if (!virtualize || isEmpty(virtualize)) {
  1082. return (
  1083. <NodeList
  1084. flattenNodes={flattenNodes}
  1085. flattenList={this._flattenNodes}
  1086. motionKeys={motionExpand ? motionKeys : new Set([])}
  1087. motionType={motionType}
  1088. onMotionEnd={this.onMotionEnd}
  1089. renderTreeNode={this.renderTreeNode}
  1090. />
  1091. );
  1092. }
  1093. const option = ({ index, style, data }: OptionProps) => this.renderTreeNode(data[index], index, style);
  1094. return (
  1095. <AutoSizer defaultHeight={virtualize.height} defaultWidth={virtualize.width}>
  1096. {({ height, width }) => (
  1097. <VirtualList
  1098. itemCount={flattenNodes.length}
  1099. itemSize={virtualize.itemSize}
  1100. height={height}
  1101. width={width}
  1102. itemKey={this.itemKey as ListItemKeySelector<TreeNodeData>}
  1103. itemData={flattenNodes as any}
  1104. className={`${prefixTree}-virtual-list`}
  1105. style={{ direction }}
  1106. >
  1107. {option}
  1108. </VirtualList>
  1109. )}
  1110. </AutoSizer>
  1111. );
  1112. };
  1113. renderTree = () => {
  1114. const { keyEntities, motionKeys, motionType, inputValue, filteredKeys, flattenNodes } = this.state;
  1115. const {
  1116. loadData,
  1117. filterTreeNode,
  1118. disabled,
  1119. multiple,
  1120. showFilteredOnly,
  1121. motionExpand,
  1122. outerBottomSlot,
  1123. outerTopSlot,
  1124. expandAction,
  1125. labelEllipsis,
  1126. virtualize,
  1127. optionListStyle,
  1128. searchPosition,
  1129. renderLabel,
  1130. renderFullLabel,
  1131. } = this.props;
  1132. const wrapperCls = cls(`${prefixTree}-wrapper`);
  1133. const listCls = cls(`${prefixTree}-option-list`, {
  1134. [`${prefixTree}-option-list-block`]: true,
  1135. });
  1136. const searchNoRes = Boolean(inputValue) && !filteredKeys.size;
  1137. const noData = isEmpty(flattenNodes) || (showFilteredOnly && searchNoRes);
  1138. const isDropdownPositionSearch = searchPosition === strings.SEARCH_POSITION_DROPDOWN;
  1139. return (
  1140. <TreeContext.Provider
  1141. value={{
  1142. loadData,
  1143. treeDisabled: disabled,
  1144. motion: motionExpand,
  1145. motionKeys,
  1146. motionType,
  1147. expandAction,
  1148. filterTreeNode,
  1149. keyEntities,
  1150. onNodeClick: this.onNodeClick,
  1151. onNodeDoubleClick: this.onNodeDoubleClick,
  1152. // tree node will call this function when treeNode is right clicked
  1153. onNodeRightClick: noop,
  1154. onNodeExpand: this.onNodeExpand,
  1155. onNodeSelect: this.onNodeSelect,
  1156. onNodeCheck: this.onNodeCheck,
  1157. renderTreeNode: this.renderTreeNode,
  1158. multiple,
  1159. showFilteredOnly,
  1160. isSearching: Boolean(inputValue),
  1161. renderLabel,
  1162. renderFullLabel,
  1163. labelEllipsis: typeof labelEllipsis === 'undefined' ? virtualize : labelEllipsis,
  1164. }}
  1165. >
  1166. <div className={wrapperCls} role="list-box">
  1167. {outerTopSlot}
  1168. {
  1169. !outerTopSlot &&
  1170. filterTreeNode &&
  1171. isDropdownPositionSearch &&
  1172. this.renderInput()
  1173. }
  1174. <div className={listCls} role="tree" style={optionListStyle}>
  1175. {noData ? this.renderEmpty() : this.renderNodeList()}
  1176. </div>
  1177. {outerBottomSlot}
  1178. </div>
  1179. </TreeContext.Provider>
  1180. );
  1181. };
  1182. render() {
  1183. const content = this.renderContent();
  1184. const {
  1185. motion,
  1186. zIndex,
  1187. mouseLeaveDelay,
  1188. mouseEnterDelay,
  1189. autoAdjustOverflow,
  1190. stopPropagation,
  1191. getPopupContainer,
  1192. } = this.props;
  1193. const { isOpen, rePosKey } = this.state;
  1194. const selection = this.renderSelection();
  1195. const pos = 'bottomLeft';
  1196. return (
  1197. <Popover
  1198. stopPropagation={stopPropagation}
  1199. getPopupContainer={getPopupContainer}
  1200. zIndex={zIndex}
  1201. motion={motion}
  1202. ref={this.optionsRef}
  1203. content={content}
  1204. visible={isOpen}
  1205. trigger="custom"
  1206. rePosKey={rePosKey}
  1207. position={pos}
  1208. autoAdjustOverflow={autoAdjustOverflow}
  1209. mouseLeaveDelay={mouseLeaveDelay}
  1210. mouseEnterDelay={mouseEnterDelay}
  1211. onVisibleChange={this.handlePopoverClose}
  1212. >
  1213. {selection}
  1214. </Popover>
  1215. );
  1216. }
  1217. }
  1218. export default TreeSelect;