index.tsx 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061
  1. /* eslint-disable max-len */
  2. /* eslint-disable max-lines-per-function */
  3. import React, { Fragment, MouseEvent, ReactInstance } from 'react';
  4. import ReactDOM from 'react-dom';
  5. import cls from 'classnames';
  6. import PropTypes from 'prop-types';
  7. import ConfigContext from '../configProvider/context';
  8. import SelectFoundation, { SelectAdapter } from '@douyinfe/semi-foundation/select/foundation';
  9. import { cssClasses, strings, numbers } from '@douyinfe/semi-foundation/select/constants';
  10. import BaseComponent, { ValidateStatus } from '../_base/baseComponent';
  11. import { isEqual, isString, noop } from 'lodash-es';
  12. import Tag from '../tag/index';
  13. import TagGroup from '../tag/group';
  14. import LocaleCosumer from '../locale/localeConsumer';
  15. import Popover from '../popover/index';
  16. import { numbers as popoverNumbers } from '@douyinfe/semi-foundation/popover/constants';
  17. import { FixedSizeList as List } from 'react-window';
  18. import { getOptionsFromGroup } from './utils';
  19. import VirtualRow from './virtualRow';
  20. import Input from '../input/index';
  21. import Option, { OptionProps } from './option';
  22. import OptionGroup from './optionGroup';
  23. import Spin from '../spin';
  24. import Trigger from '../trigger';
  25. import { IconChevronDown, IconClear } from '@douyinfe/semi-icons';
  26. import { isSemiIcon } from '../_utils';
  27. import warning from '@douyinfe/semi-foundation/utils/warning';
  28. import '@douyinfe/semi-foundation/select/select.scss';
  29. import '@douyinfe/semi-foundation/select/option.scss';
  30. import { Locale } from '../locale/interface';
  31. import { Position, TooltipProps } from '../tooltip';
  32. export { OptionProps } from './option';
  33. export { OptionGroupProps } from './optionGroup';
  34. export { VirtualRowProps } from './virtualRow';
  35. const prefixcls = cssClasses.PREFIX;
  36. const key = 0;
  37. type OnChangeValueType = string | number | Record<string, any>;
  38. export interface optionRenderProps {
  39. key?: any;
  40. label?: string | React.ReactNode | number;
  41. value?: string | number;
  42. style?: React.CSSProperties;
  43. className?: string;
  44. selected?: boolean;
  45. focused?: boolean;
  46. show?: boolean;
  47. disabled?: boolean;
  48. onMouseEnter?: (e: React.MouseEvent) => any;
  49. onClick?: (e: React.MouseEvent) => any;
  50. [x: string]: any;
  51. }
  52. export interface selectMethod {
  53. clearInput?: () => void;
  54. selectAll?: () => void;
  55. deselectAll?: () => void;
  56. focus?: () => void;
  57. close?: () => void;
  58. open?: () => void;
  59. }
  60. export type SelectSize = 'small' | 'large' | 'default';
  61. export interface virtualListProps {
  62. itemSize?: number;
  63. height?: number;
  64. width?: string | number;
  65. }
  66. export interface RenderSelectedItemFn {
  67. (optionNode: Record<string, any>): React.ReactNode;
  68. (optionNode: Record<string, any>, multipleProps: { index: number } & Record<string, any>): { isRenderInTag: boolean; content: React.ReactNode };
  69. }
  70. export type SelectProps = {
  71. autoFocus?: boolean;
  72. arrowIcon?: React.ReactNode;
  73. defaultValue?: string | number | any[] | Record<string, any>;
  74. value?: string | number | any[] | Record<string, any>;
  75. placeholder?: React.ReactNode;
  76. onChange?: (value: SelectProps['value']) => void;
  77. multiple?: boolean;
  78. filter?: boolean | ((inpueValue: string, option: OptionProps) => boolean);
  79. max?: number;
  80. maxTagCount?: number;
  81. maxHeight?: string | number;
  82. style?: React.CSSProperties;
  83. className?: string;
  84. size?: SelectSize;
  85. disabled?: boolean;
  86. emptyContent?: React.ReactNode;
  87. onDropdownVisibleChange?: (visible: boolean) => void;
  88. zIndex?: number;
  89. position?: Position;
  90. onSearch?: (value: string) => void;
  91. dropdownClassName?: string;
  92. dropdownStyle?: React.CSSProperties;
  93. outerTopSlot?: React.ReactNode;
  94. innerTopSlot?: React.ReactNode;
  95. outerBottomSlot?: React.ReactNode;
  96. innerBottomSlot?: React.ReactNode;
  97. optionList?: OptionProps[];
  98. dropdownMatchSelectWidth?: boolean;
  99. loading?: boolean;
  100. defaultOpen?: boolean;
  101. validateStatus?: ValidateStatus;
  102. defaultActiveFirstOption?: boolean;
  103. onChangeWithObject?: boolean;
  104. suffix?: React.ReactNode;
  105. prefix?: React.ReactNode;
  106. insetLabel?: React.ReactNode;
  107. showClear?: boolean;
  108. showArrow?: boolean;
  109. renderSelectedItem?: RenderSelectedItemFn;
  110. renderCreateItem?: (inputValue: OptionProps['value'], focus: boolean) => React.ReactNode;
  111. renderOptionItem?: (props: optionRenderProps) => React.ReactNode;
  112. onMouseEnter?: (e: React.MouseEvent) => any;
  113. onMouseLeave?: (e: React.MouseEvent) => any;
  114. clickToHide?: boolean;
  115. onExceed?: (option: OptionProps) => void;
  116. onCreate?: (option: OptionProps) => void;
  117. remote?: boolean;
  118. onDeselect?: (value: SelectProps['value'], option: Record<string, any>) => void;
  119. onSelect?: (value: SelectProps['value'], option: Record<string, any>) => void;
  120. allowCreate?: boolean;
  121. triggerRender?: (props?: any) => React.ReactNode;
  122. onClear?: () => void;
  123. virtualize?: virtualListProps;
  124. onFocus?: (e: React.FocusEvent) => void;
  125. onBlur?: (e: React.FocusEvent) => void;
  126. onListScroll?: (e: React.UIEvent<HTMLDivElement>) => void;
  127. children?: React.ReactNode;
  128. } & Pick<
  129. TooltipProps,
  130. | 'spacing'
  131. | 'getPopupContainer'
  132. | 'motion'
  133. | 'autoAdjustOverflow'
  134. | 'mouseLeaveDelay'
  135. | 'mouseEnterDelay'
  136. | 'stopPropagation'
  137. > & React.RefAttributes<any>;
  138. export interface SelectState {
  139. isOpen: boolean;
  140. isFocus: boolean;
  141. options: Array<OptionProps>;
  142. selections: Map<OptionProps['label'], any>; // A collection of all currently selected items, k: label, v: {value,... otherProps}
  143. dropdownMinWidth: number;
  144. optionKey: number;
  145. inputValue: string;
  146. showInput: boolean;
  147. focusIndex: number;
  148. keyboardEventSet: any; // {}
  149. optionGroups: Array<any>;
  150. isHovering: boolean;
  151. }
  152. // Notes: Use the label of the option as the identifier, that is, the option in Select, the value is allowed to be the same, but the label must be unique
  153. class Select extends BaseComponent<SelectProps, SelectState> {
  154. static contextType = ConfigContext;
  155. static Option = Option;
  156. static OptGroup = OptionGroup;
  157. static propTypes = {
  158. autoFocus: PropTypes.bool,
  159. children: PropTypes.node,
  160. defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array, PropTypes.object]),
  161. value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array, PropTypes.object]),
  162. placeholder: PropTypes.node,
  163. onChange: PropTypes.func,
  164. multiple: PropTypes.bool,
  165. // Whether to turn on the input box filtering function, when it is a function, it represents a custom filtering function
  166. filter: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
  167. // How many tags can you choose?
  168. max: PropTypes.number,
  169. // How many tabs are displayed at most, and the rest are displayed in + N
  170. maxTagCount: PropTypes.number,
  171. maxHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  172. style: PropTypes.object,
  173. className: PropTypes.string,
  174. size: PropTypes.oneOf<SelectProps['size']>(strings.SIZE_SET),
  175. disabled: PropTypes.bool,
  176. emptyContent: PropTypes.node,
  177. onDropdownVisibleChange: PropTypes.func,
  178. zIndex: PropTypes.number,
  179. position: PropTypes.oneOf(strings.POSITION_SET),
  180. onSearch: PropTypes.func,
  181. getPopupContainer: PropTypes.func,
  182. dropdownClassName: PropTypes.string,
  183. dropdownStyle: PropTypes.object,
  184. outerTopSlot: PropTypes.node,
  185. innerTopSlot: PropTypes.node,
  186. outerBottomSlot: PropTypes.node,
  187. innerBottomSlot: PropTypes.node, // Options slot
  188. optionList: PropTypes.array,
  189. dropdownMatchSelectWidth: PropTypes.bool,
  190. loading: PropTypes.bool,
  191. defaultOpen: PropTypes.bool,
  192. validateStatus: PropTypes.oneOf(strings.STATUS),
  193. defaultActiveFirstOption: PropTypes.bool,
  194. triggerRender: PropTypes.func,
  195. stopPropagation: PropTypes.bool,
  196. // motion doesn't need to be exposed
  197. motion: PropTypes.oneOfType([PropTypes.func, PropTypes.bool, PropTypes.object]),
  198. onChangeWithObject: PropTypes.bool,
  199. suffix: PropTypes.node,
  200. prefix: PropTypes.node,
  201. insetLabel: PropTypes.node,
  202. showClear: PropTypes.bool,
  203. showArrow: PropTypes.bool,
  204. renderSelectedItem: PropTypes.func,
  205. allowCreate: PropTypes.bool,
  206. renderCreateItem: PropTypes.func,
  207. onMouseEnter: PropTypes.func,
  208. onMouseLeave: PropTypes.func,
  209. clickToHide: PropTypes.bool,
  210. onExceed: PropTypes.func,
  211. onCreate: PropTypes.func,
  212. remote: PropTypes.bool,
  213. onDeselect: PropTypes.func,
  214. // The main difference between onSelect and onChange is that when multiple selections are selected, onChange contains all options, while onSelect only contains items for the current operation
  215. onSelect: PropTypes.func,
  216. autoAdjustOverflow: PropTypes.bool,
  217. mouseEnterDelay: PropTypes.number,
  218. mouseLeaveDelay: PropTypes.number,
  219. spacing: PropTypes.number,
  220. onBlur: PropTypes.func,
  221. onFocus: PropTypes.func,
  222. onClear: PropTypes.func,
  223. virtualize: PropTypes.object,
  224. renderOptionItem: PropTypes.func,
  225. onListScroll: PropTypes.func,
  226. arrowIcon: PropTypes.node,
  227. // open: PropTypes.bool,
  228. // tagClosable: PropTypes.bool,
  229. };
  230. static defaultProps: Partial<SelectProps> = {
  231. stopPropagation: true,
  232. motion: true,
  233. zIndex: popoverNumbers.DEFAULT_Z_INDEX,
  234. // position: 'bottomLeft',
  235. filter: false,
  236. multiple: false,
  237. disabled: false,
  238. defaultOpen: false,
  239. allowCreate: false,
  240. placeholder: '',
  241. onDropdownVisibleChange: noop,
  242. onChangeWithObject: false,
  243. onChange: noop,
  244. onSearch: noop,
  245. onMouseEnter: noop,
  246. onMouseLeave: noop,
  247. onDeselect: noop,
  248. onSelect: noop,
  249. onCreate: noop,
  250. onExceed: noop,
  251. onFocus: noop,
  252. onBlur: noop,
  253. onClear: noop,
  254. onListScroll: noop,
  255. maxHeight: 300,
  256. dropdownMatchSelectWidth: true,
  257. defaultActiveFirstOption: false,
  258. showArrow: true,
  259. showClear: false,
  260. remote: false,
  261. autoAdjustOverflow: true,
  262. arrowIcon: <IconChevronDown />
  263. // Radio selection is different from the default renderSelectedItem for multiple selection, so it is not declared here
  264. // renderSelectedItem: (optionNode) => optionNode.label,
  265. // The default creator rendering is related to i18, so it is not declared here
  266. // renderCreateItem: (input) => input
  267. };
  268. inputRef: React.RefObject<HTMLInputElement>;
  269. triggerRef: React.RefObject<HTMLDivElement>;
  270. optionsRef: React.RefObject<any>;
  271. clickOutsideHandler: (e: MouseEvent) => void;
  272. foundation: SelectFoundation;
  273. constructor(props: SelectProps) {
  274. super(props);
  275. this.state = {
  276. isOpen: false,
  277. isFocus: false,
  278. options: [], // All options
  279. selections: new Map(), // A collection of all currently selected items, k: label, v: {value,... otherProps}
  280. dropdownMinWidth: null,
  281. optionKey: key,
  282. inputValue: '',
  283. showInput: false,
  284. focusIndex: props.defaultActiveFirstOption ? 0 : -1,
  285. keyboardEventSet: {},
  286. optionGroups: [],
  287. isHovering: false,
  288. };
  289. this.inputRef = React.createRef();
  290. this.triggerRef = React.createRef();
  291. this.optionsRef = React.createRef();
  292. this.clickOutsideHandler = null;
  293. this.onSelect = this.onSelect.bind(this);
  294. this.onClear = this.onClear.bind(this);
  295. this.onMouseEnter = this.onMouseEnter.bind(this);
  296. this.onMouseLeave = this.onMouseLeave.bind(this);
  297. this.renderOption = this.renderOption.bind(this);
  298. this.foundation = new SelectFoundation(this.adapter);
  299. warning(
  300. 'optionLabelProp' in this.props,
  301. '[Semi Select] \'optionLabelProp\' has already been deprecated, please use \'renderSelectedItem\' instead.'
  302. );
  303. warning(
  304. 'labelInValue' in this.props,
  305. '[Semi Select] \'labelInValue\' has already been deprecated, please use \'onChangeWithObject\' instead.'
  306. );
  307. }
  308. get adapter(): SelectAdapter<SelectProps, SelectState> {
  309. const keyboardAdapter = {
  310. registerKeyDown: (cb: () => void) => {
  311. const keyboardEventSet = {
  312. onKeyDown: cb,
  313. };
  314. this.setState({ keyboardEventSet });
  315. },
  316. unregisterKeyDown: () => {
  317. this.setState({ keyboardEventSet: {} });
  318. },
  319. updateFocusIndex: (focusIndex: number) => {
  320. this.setState({ focusIndex });
  321. },
  322. // eslint-disable-next-line @typescript-eslint/no-empty-function
  323. scrollToFocusOption: () => {},
  324. };
  325. const filterAdapter = {
  326. updateInputValue: (value: string) => {
  327. this.setState({ inputValue: value });
  328. },
  329. toggleInputShow: (showInput: boolean, cb: (...args: any) => void) => {
  330. this.setState({ showInput }, () => {
  331. cb();
  332. });
  333. },
  334. focusInput: () => {
  335. if (this.inputRef && this.inputRef.current) {
  336. this.inputRef.current.focus();
  337. }
  338. },
  339. };
  340. const multipleAdapter = {
  341. notifyMaxLimit: (option: OptionProps) => this.props.onExceed(option),
  342. getMaxLimit: () => this.props.max,
  343. registerClickOutsideHandler: (cb: (e: MouseEvent) => void) => {
  344. const clickOutsideHandler: (e: MouseEvent) => void = e => {
  345. const optionInstance = this.optionsRef && this.optionsRef.current;
  346. const triggerDom = (this.triggerRef && this.triggerRef.current) as Element;
  347. // eslint-disable-next-line react/no-find-dom-node
  348. const optionsDom = ReactDOM.findDOMNode(optionInstance as ReactInstance);
  349. // let isInPanel = optionsDom && optionsDom.contains(e.target);
  350. // let isInTrigger = triggerDom && triggerDom.contains(e.target);
  351. if (optionsDom && !optionsDom.contains(e.target as Node) &&
  352. triggerDom && !triggerDom.contains(e.target as Node)) {
  353. cb(e);
  354. }
  355. };
  356. this.clickOutsideHandler = clickOutsideHandler;
  357. document.addEventListener('mousedown', clickOutsideHandler as any, false);
  358. },
  359. unregisterClickOutsideHandler: () => {
  360. if (this.clickOutsideHandler) {
  361. document.removeEventListener('mousedown', this.clickOutsideHandler as any, false);
  362. this.clickOutsideHandler = null;
  363. }
  364. },
  365. rePositionDropdown: () => {
  366. let { optionKey } = this.state;
  367. optionKey = optionKey + 1;
  368. this.setState({ optionKey });
  369. },
  370. notifyDeselect: (value: OptionProps['value'], option: OptionProps) => {
  371. delete option._parentGroup;
  372. this.props.onDeselect(value, option);
  373. },
  374. };
  375. return {
  376. ...super.adapter,
  377. ...keyboardAdapter,
  378. ...filterAdapter,
  379. ...multipleAdapter,
  380. // Collect all subitems, each item is visible by default when collected, and is not selected
  381. getOptionsFromChildren: (children = this.props.children) => {
  382. let optionGroups = [];
  383. let options = [];
  384. const { optionList } = this.props;
  385. if (optionList && optionList.length) {
  386. options = optionList.map(itemOpt => ({ _show: true, _selected: false, ...itemOpt }));
  387. optionGroups[0] = { children: options, label: '' };
  388. } else {
  389. const result = getOptionsFromGroup(children);
  390. optionGroups = result.optionGroups;
  391. options = result.options;
  392. }
  393. this.setState({ optionGroups });
  394. return options;
  395. },
  396. updateOptions: (options: OptionProps[]) => {
  397. this.setState({ options });
  398. },
  399. openMenu: () => {
  400. this.setState({ isOpen: true });
  401. },
  402. closeMenu: () => {
  403. this.setState({ isOpen: false });
  404. },
  405. getTriggerWidth: () => {
  406. const el = this.triggerRef.current;
  407. return el && el.getBoundingClientRect().width;
  408. },
  409. setOptionWrapperWidth: (width: number) => {
  410. this.setState({ dropdownMinWidth: width });
  411. },
  412. updateSelection: (selections: Map<OptionProps['label'], any>) => {
  413. this.setState({ selections });
  414. },
  415. // clone Map, important!!!, prevent unexpected modify on state
  416. getSelections: () => new Map(this.state.selections),
  417. notifyChange: (value: OnChangeValueType | OnChangeValueType[]) => {
  418. this.props.onChange(value);
  419. },
  420. notifySelect: (value: OptionProps['value'], option: OptionProps) => {
  421. delete option._parentGroup;
  422. this.props.onSelect(value, option);
  423. },
  424. notifyDropdownVisibleChange: (visible: boolean) => {
  425. this.props.onDropdownVisibleChange(visible);
  426. },
  427. notifySearch: (input: string) => {
  428. this.props.onSearch(input);
  429. },
  430. notifyCreate: (input: OptionProps) => {
  431. this.props.onCreate(input);
  432. },
  433. notifyMouseEnter: (e: React.MouseEvent<HTMLDivElement>) => {
  434. this.props.onMouseEnter(e);
  435. },
  436. notifyMouseLeave: (e: React.MouseEvent<HTMLDivElement>) => {
  437. this.props.onMouseLeave(e);
  438. },
  439. notifyFocus: (event: React.FocusEvent) => {
  440. this.props.onFocus(event);
  441. },
  442. notifyBlur: (event: React.FocusEvent) => {
  443. this.props.onBlur(event);
  444. },
  445. notifyClear: () => {
  446. this.props.onClear();
  447. },
  448. notifyListScroll: (e: React.UIEvent<HTMLDivElement>) => {
  449. this.props.onListScroll(e);
  450. },
  451. updateHovering: (isHovering: boolean) => {
  452. this.setState({ isHovering });
  453. },
  454. updateFocusState: (isFocus: boolean) => {
  455. this.setState({ isFocus });
  456. },
  457. focusTrigger: () => {
  458. try {
  459. const el = (this.triggerRef.current) as any;
  460. el.focus();
  461. } catch (error) {
  462. }
  463. }
  464. };
  465. }
  466. componentDidMount() {
  467. this.foundation.init();
  468. }
  469. componentWillUnmount() {
  470. this.foundation.destroy();
  471. }
  472. componentDidUpdate(prevProps: SelectProps, prevState: SelectState) {
  473. const prevChildrenKeys = React.Children.toArray(prevProps.children).map((child: any) => child.key);
  474. const nowChildrenKeys = React.Children.toArray(this.props.children).map((child: any) => child.key);
  475. let isOptionsChanged = false;
  476. if (!isEqual(prevChildrenKeys, nowChildrenKeys) || !isEqual(prevProps.optionList, this.props.optionList)) {
  477. isOptionsChanged = true;
  478. this.foundation.handleOptionListChange();
  479. }
  480. // Add isOptionChanged: There may be cases where the value is unchanged, but the optionList is updated. At this time, the label corresponding to the value may change, and the selected item needs to be updated
  481. if (prevProps.value !== this.props.value || isOptionsChanged) {
  482. if ('value' in this.props) {
  483. this.foundation.handleValueChange(this.props.value as any);
  484. } else {
  485. this.foundation.handleOptionListChangeHadDefaultValue();
  486. }
  487. }
  488. }
  489. handleInputChange = (value: string) => this.foundation.handleInputChange(value);
  490. renderInput() {
  491. const { size, multiple, disabled } = this.props;
  492. const inputcls = cls(`${prefixcls}-input`, {
  493. [`${prefixcls}-input-single`]: !multiple,
  494. [`${prefixcls}-input-multiple`]: multiple,
  495. });
  496. const { inputValue } = this.state;
  497. const inputProps: Record<string, any> = {
  498. value: inputValue,
  499. disabled,
  500. className: inputcls,
  501. onChange: this.handleInputChange,
  502. };
  503. let style = {};
  504. // Multiple choice mode
  505. if (multiple) {
  506. style = {
  507. width: inputValue ? `${inputValue.length * 16}px` : '2px',
  508. };
  509. inputProps.style = style;
  510. }
  511. return (
  512. <Input
  513. ref={this.inputRef as any}
  514. size={size}
  515. {...inputProps}
  516. onFocus={(e: React.FocusEvent<HTMLInputElement>) => {
  517. // prevent event bubbling which will fire trigger onFocus event
  518. e.stopPropagation();
  519. // e.nativeEvent.stopImmediatePropagation();
  520. }}
  521. />
  522. );
  523. }
  524. close() {
  525. this.foundation.close();
  526. }
  527. open() {
  528. this.foundation.open();
  529. }
  530. clearInput() {
  531. this.foundation.clearInput();
  532. }
  533. selectAll() {
  534. this.foundation.selectAll();
  535. }
  536. deselectAll() {
  537. this.foundation.clearSelected();
  538. }
  539. focus() {
  540. this.foundation.focus();
  541. }
  542. onSelect(option: OptionProps, optionIndex: number, e: any) {
  543. this.foundation.onSelect(option, optionIndex, e);
  544. }
  545. onClear(e: React.MouseEvent) {
  546. e.nativeEvent.stopImmediatePropagation();
  547. this.foundation.handleClearClick(e as any);
  548. }
  549. renderEmpty() {
  550. return <Option empty={true} emptyContent={this.props.emptyContent} />;
  551. }
  552. renderLoading() {
  553. const loadingWrapperCls = `${prefixcls}-loading-wrapper`;
  554. return (
  555. <div className={loadingWrapperCls}>
  556. <Spin />
  557. </div>
  558. );
  559. }
  560. renderOption(option: OptionProps, optionIndex: number, style?: React.CSSProperties) {
  561. const { focusIndex, inputValue } = this.state;
  562. const { renderOptionItem } = this.props;
  563. let optionContent;
  564. const isFocused = optionIndex === focusIndex;
  565. let optionStyle = style || {};
  566. if (option.style) {
  567. optionStyle = { ...optionStyle, ...option.style };
  568. }
  569. if (option._inputCreateOnly) {
  570. optionContent = this.renderCreateOption(option, isFocused, optionIndex, style);
  571. } else {
  572. // use another name to make sure that 'key' in optionList still exist when we call onChange
  573. if ('key' in option) {
  574. option._keyInOptionList = option.key;
  575. }
  576. optionContent = (
  577. <Option
  578. showTick
  579. {...option}
  580. selected={option._selected}
  581. onSelect={(v: OptionProps, e: MouseEvent) => this.onSelect(v, optionIndex, e)}
  582. focused={isFocused}
  583. onMouseEnter={() => this.onOptionHover(optionIndex)}
  584. style={optionStyle}
  585. key={option.key || option.label as string + option.value as string + optionIndex}
  586. renderOptionItem={renderOptionItem}
  587. inputValue={inputValue}
  588. >
  589. {option.label}
  590. </Option>
  591. );
  592. }
  593. return optionContent;
  594. }
  595. renderCreateOption(option: OptionProps, isFocused: boolean, optionIndex: number, style: React.CSSProperties) {
  596. const { renderCreateItem } = this.props;
  597. // default render method
  598. if (typeof renderCreateItem === 'undefined') {
  599. const defaultCreateItem = (
  600. <Option
  601. key={option.key || option.label as string + option.value as string}
  602. onSelect={(v: OptionProps, e: MouseEvent) => this.onSelect(v, optionIndex, e)}
  603. onMouseEnter={() => this.onOptionHover(optionIndex)}
  604. showTick
  605. {...option}
  606. focused={isFocused}
  607. style={style}
  608. >
  609. <LocaleCosumer componentName="Select">
  610. {(locale: Locale['Select']) => (
  611. <>
  612. <span className={`${prefixcls}-create-tips`}>{locale.createText}</span>
  613. {option.value}
  614. </>
  615. )}
  616. </LocaleCosumer>
  617. </Option>
  618. );
  619. return defaultCreateItem;
  620. }
  621. const customCreateItem = renderCreateItem(option.value, isFocused);
  622. return (
  623. <div onClick={e => this.onSelect(option, optionIndex, e)} key={new Date().valueOf()}>
  624. {customCreateItem}
  625. </div>
  626. );
  627. }
  628. onOptionHover(optionIndex: number) {
  629. this.foundation.handleOptionMouseEnter(optionIndex);
  630. }
  631. renderWithGroup(visibileOptions: OptionProps[]) {
  632. const content: JSX.Element[] = [];
  633. const groupStatus = new Map();
  634. visibileOptions.forEach((option, optionIndex) => {
  635. const parentGroup = option._parentGroup;
  636. const optionContent = this.renderOption(option, optionIndex);
  637. if (parentGroup && groupStatus.has(parentGroup.label)) {
  638. // group content already insert
  639. content.push(optionContent);
  640. } else if (parentGroup) {
  641. const groupContent = <OptionGroup {...parentGroup} key={parentGroup.label} />;
  642. groupStatus.set(parentGroup.label, true);
  643. content.push(groupContent);
  644. content.push(optionContent);
  645. } else {
  646. // when not use with OptionGroup
  647. content.push(optionContent);
  648. }
  649. });
  650. return content;
  651. }
  652. renderVirtualizeList(visibileOptions: OptionProps[]) {
  653. const { virtualize } = this.props;
  654. const { direction } = this.context;
  655. const { height, width, itemSize } = virtualize;
  656. return (
  657. <List
  658. height={height || numbers.LIST_HEIGHT}
  659. itemCount={visibileOptions.length}
  660. itemSize={itemSize}
  661. itemData={{ visibileOptions, renderOption: this.renderOption }}
  662. width={width || '100%'}
  663. style={{ direction }}
  664. >
  665. {VirtualRow}
  666. </List>
  667. );
  668. }
  669. renderOptions(children?: React.ReactNode) {
  670. const { dropdownMinWidth, options, selections } = this.state;
  671. const {
  672. maxHeight,
  673. dropdownClassName,
  674. dropdownStyle,
  675. outerTopSlot,
  676. innerTopSlot,
  677. outerBottomSlot,
  678. innerBottomSlot,
  679. loading,
  680. virtualize,
  681. } = this.props;
  682. // Do a filter first, instead of directly judging in forEach, so that the focusIndex can correspond to
  683. const visibileOptions = options.filter(item => item._show);
  684. let listContent: JSX.Element | JSX.Element[] = this.renderWithGroup(visibileOptions);
  685. if (virtualize) {
  686. listContent = this.renderVirtualizeList(visibileOptions);
  687. }
  688. const style = { minWidth: dropdownMinWidth, ...dropdownStyle };
  689. const optionListCls = cls({
  690. [`${prefixcls}-option-list`]: true,
  691. [`${prefixcls}-option-list-chosen`]: selections.size,
  692. });
  693. const isEmpty = !options.length || !options.some(item => item._show);
  694. return (
  695. <div className={dropdownClassName} style={style}>
  696. {outerTopSlot}
  697. <div
  698. style={{ maxHeight: `${maxHeight}px` }}
  699. className={optionListCls}
  700. role="listbox"
  701. onScroll={e => this.foundation.handleListScroll(e)}
  702. >
  703. {innerTopSlot}
  704. {!loading ? listContent : this.renderLoading()}
  705. {isEmpty && !loading ? this.renderEmpty() : null}
  706. {innerBottomSlot}
  707. </div>
  708. {outerBottomSlot}
  709. </div>
  710. );
  711. }
  712. renderSingleSelection(selections: Map<OptionProps['label'], any>, filterable: boolean) {
  713. let { renderSelectedItem } = this.props;
  714. const { placeholder } = this.props;
  715. const { showInput, inputValue } = this.state;
  716. let renderText: React.ReactNode = '';
  717. const selectedItems = [...selections];
  718. if (typeof renderSelectedItem === 'undefined') {
  719. renderSelectedItem = ((optionNode: OptionProps) => optionNode.label) as RenderSelectedItemFn;
  720. }
  721. if (selectedItems.length) {
  722. const selectedItem = selectedItems[0][1];
  723. renderText = renderSelectedItem(selectedItem);
  724. }
  725. const spanCls = cls({
  726. [`${prefixcls}-selection-text`]: true,
  727. [`${prefixcls}-selection-placeholder`]: !renderText && renderText !== 0,
  728. [`${prefixcls}-selection-text-hide`]: inputValue && showInput, // show Input
  729. [`${prefixcls}-selection-text-inactive`]: !inputValue && showInput, // Stack Input & RenderText(opacity 0.4)
  730. });
  731. const contentWrapperCls = `${prefixcls}-content-wrapper`;
  732. return (
  733. <>
  734. <div className={contentWrapperCls}>
  735. {<span className={spanCls}>{renderText || renderText === 0 ? renderText : placeholder}</span>}
  736. {filterable && showInput ? this.renderInput() : null}
  737. </div>
  738. </>
  739. );
  740. }
  741. renderMultipleSelection(selections: Map<OptionProps['label'], any>, filterable: boolean) {
  742. let { renderSelectedItem } = this.props;
  743. const { placeholder, maxTagCount, size } = this.props;
  744. const { inputValue } = this.state;
  745. const selectDisabled = this.props.disabled;
  746. const renderTags = [];
  747. const selectedItems = [...selections];
  748. if (typeof renderSelectedItem === 'undefined') {
  749. renderSelectedItem = (optionNode: OptionProps) => ({
  750. isRenderInTag: true,
  751. content: optionNode.label,
  752. });
  753. }
  754. const tags = selectedItems.map((item, i) => {
  755. const label = item[0];
  756. const { value } = item[1];
  757. const disabled = item[1].disabled || selectDisabled;
  758. const onClose = (tagContent: React.ReactNode, e: MouseEvent) => {
  759. if (e && typeof e.preventDefault === 'function') {
  760. e.preventDefault(); // make sure that tag will not hidden immediately in controlled mode
  761. }
  762. this.foundation.removeTag({ label, value });
  763. };
  764. const { content, isRenderInTag } = renderSelectedItem(item[1], { index: i, disabled, onClose });
  765. const basic = {
  766. disabled,
  767. closable: !disabled,
  768. onClose,
  769. };
  770. if (isRenderInTag) {
  771. return (
  772. <Tag {...basic} color="white" size={size || 'large'} key={value}>
  773. {content}
  774. </Tag>
  775. );
  776. } else {
  777. return content;
  778. }
  779. });
  780. const contentWrapperCls = cls({
  781. [`${prefixcls}-content-wrapper`]: true,
  782. [`${prefixcls}-content-wrapper-one-line`]: maxTagCount,
  783. [`${prefixcls}-content-wrapper-empty`]: !tags.length,
  784. });
  785. const spanCls = cls({
  786. [`${prefixcls}-selection-text`]: true,
  787. [`${prefixcls}-selection-placeholder`]: !tags.length,
  788. [`${prefixcls}-selection-text-hide`]: tags && tags.length,
  789. // [prefixcls + '-selection-text-inactive']: !inputValue && !tags.length,
  790. });
  791. const placeholderText = placeholder && !inputValue ? <span className={spanCls}>{placeholder}</span> : null;
  792. const n = tags.length > maxTagCount ? maxTagCount : undefined;
  793. const NotOneLine = !maxTagCount; // Multiple lines (that is, do not set maxTagCount), do not use TagGroup, directly traverse with Tag, otherwise Input cannot follow the correct position
  794. const tagContent = NotOneLine ? tags : <TagGroup tagList={tags} maxTagCount={n} size="large" mode="custom" />;
  795. return (
  796. <>
  797. <div className={contentWrapperCls}>
  798. {tags && tags.length ? tagContent : placeholderText}
  799. {!filterable ? null : this.renderInput()}
  800. </div>
  801. </>
  802. );
  803. }
  804. onMouseEnter(e: MouseEvent) {
  805. this.foundation.handleMouseEnter(e as any);
  806. }
  807. onMouseLeave(e: MouseEvent) {
  808. this.foundation.handleMouseLeave(e as any);
  809. }
  810. renderSuffix() {
  811. const { suffix } = this.props;
  812. const suffixWrapperCls = cls({
  813. [`${prefixcls}-suffix`]: true,
  814. [`${prefixcls}-suffix-text`]: suffix && isString(suffix),
  815. [`${prefixcls}-suffix-icon`]: isSemiIcon(suffix),
  816. });
  817. return <div className={suffixWrapperCls}>{suffix}</div>;
  818. }
  819. renderPrefix() {
  820. const { prefix, insetLabel } = this.props;
  821. const labelNode = (prefix || insetLabel) as React.ReactElement<any, any>;
  822. const prefixWrapperCls = cls({
  823. [`${prefixcls}-prefix`]: true,
  824. [`${prefixcls}-inset-label`]: insetLabel,
  825. [`${prefixcls}-prefix-text`]: labelNode && isString(labelNode),
  826. [`${prefixcls}-prefix-icon`]: isSemiIcon(labelNode),
  827. });
  828. return <div className={prefixWrapperCls}>{labelNode}</div>;
  829. }
  830. renderSelection() {
  831. const {
  832. disabled,
  833. multiple,
  834. filter,
  835. style,
  836. size,
  837. className,
  838. validateStatus,
  839. showArrow,
  840. suffix,
  841. prefix,
  842. insetLabel,
  843. placeholder,
  844. triggerRender,
  845. arrowIcon,
  846. } = this.props;
  847. const { selections, isOpen, keyboardEventSet, inputValue, isHovering, isFocus } = this.state;
  848. const useCustomTrigger = typeof triggerRender === 'function';
  849. const filterable = Boolean(filter); // filter(boolean || function)
  850. const selectionCls = useCustomTrigger ?
  851. cls(className) :
  852. cls(prefixcls, className, {
  853. [`${prefixcls}-open`]: isOpen,
  854. [`${prefixcls}-focus`]: isFocus,
  855. [`${prefixcls}-disabled`]: disabled,
  856. [`${prefixcls}-single`]: !multiple,
  857. [`${prefixcls}-multiple`]: multiple,
  858. [`${prefixcls}-filterable`]: filterable,
  859. [`${prefixcls}-small`]: size === 'small',
  860. [`${prefixcls}-large`]: size === 'large',
  861. [`${prefixcls}-error`]: validateStatus === 'error',
  862. [`${prefixcls}-warning`]: validateStatus === 'warning',
  863. [`${prefixcls}-no-arrow`]: !showArrow,
  864. [`${prefixcls}-with-prefix`]: prefix || insetLabel,
  865. [`${prefixcls}-with-suffix`]: suffix,
  866. });
  867. const showClear = this.props.showClear &&
  868. (selections.size || inputValue) && !disabled && (isHovering || isOpen);
  869. const arrowContent = showArrow ? (
  870. <div className={`${prefixcls}-arrow`}>
  871. {arrowIcon}
  872. </div>
  873. ) : (
  874. <div className={`${prefixcls}-arrow-empty`} />
  875. );
  876. const inner = useCustomTrigger ? (
  877. <Trigger
  878. value={Array.from(selections.values())}
  879. inputValue={inputValue}
  880. onChange={this.handleInputChange}
  881. onClear={this.onClear}
  882. disabled={disabled}
  883. triggerRender={triggerRender}
  884. placeholder={placeholder as any}
  885. componentName="Select"
  886. componentProps={{ ...this.props }}
  887. />
  888. ) : (
  889. [
  890. <Fragment key="prefix">{prefix || insetLabel ? this.renderPrefix() : null}</Fragment>,
  891. <Fragment key="selection">
  892. <div className={cls(`${prefixcls}-selection`)}>
  893. {multiple ?
  894. this.renderMultipleSelection(selections, filterable) :
  895. this.renderSingleSelection(selections, filterable)}
  896. </div>
  897. </Fragment>,
  898. <Fragment key="clearicon">
  899. {showClear ? (
  900. <div className={cls(`${prefixcls}-clear`)} onClick={this.onClear}>
  901. <IconClear />
  902. </div>
  903. ) : arrowContent}
  904. </Fragment>,
  905. <Fragment key="suffix">{suffix ? this.renderSuffix() : null}</Fragment>,
  906. ]
  907. );
  908. const tabIndex = disabled ? null : 0;
  909. return (
  910. <div
  911. className={selectionCls}
  912. ref={ref => ((this.triggerRef as any).current = ref)}
  913. onClick={e => this.foundation.handleClick(e)}
  914. style={style}
  915. tabIndex={tabIndex}
  916. onMouseEnter={this.onMouseEnter}
  917. onMouseLeave={this.onMouseLeave}
  918. // onFocus={e => this.foundation.handleTriggerFocus(e)}
  919. onBlur={e => this.foundation.handleTriggerBlur(e as any)}
  920. {...keyboardEventSet}
  921. >
  922. {inner}
  923. </div>
  924. );
  925. }
  926. render() {
  927. const { direction } = this.context;
  928. const defaultPosition = direction === 'rtl' ? 'bottomRight' : 'bottomLeft';
  929. const {
  930. children,
  931. position = defaultPosition,
  932. zIndex,
  933. getPopupContainer,
  934. motion,
  935. autoAdjustOverflow,
  936. mouseLeaveDelay,
  937. mouseEnterDelay,
  938. spacing,
  939. stopPropagation,
  940. } = this.props;
  941. const { isOpen, optionKey } = this.state;
  942. const optionList = this.renderOptions(children);
  943. const selection = this.renderSelection();
  944. return (
  945. <Popover
  946. getPopupContainer={getPopupContainer}
  947. motion={motion}
  948. autoAdjustOverflow={autoAdjustOverflow}
  949. mouseLeaveDelay={mouseLeaveDelay}
  950. mouseEnterDelay={mouseEnterDelay}
  951. // transformFromCenter TODO: check no such property
  952. zIndex={zIndex}
  953. ref={this.optionsRef}
  954. content={optionList}
  955. visible={isOpen}
  956. trigger="custom"
  957. rePosKey={optionKey}
  958. position={position}
  959. spacing={spacing}
  960. stopPropagation={stopPropagation}
  961. >
  962. {selection}
  963. </Popover>
  964. );
  965. }
  966. }
  967. export default Select;