index.tsx 38 KB

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