index.tsx 41 KB

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