index.tsx 41 KB

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