index.tsx 61 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542
  1. import React, { Fragment, MouseEvent, ReactInstance, ReactNode } from 'react';
  2. import ReactDOM from 'react-dom';
  3. import cls from 'classnames';
  4. import PropTypes from 'prop-types';
  5. import ConfigContext, { ContextValue } from '../configProvider/context';
  6. import SelectFoundation, { SelectAdapter } from '@douyinfe/semi-foundation/select/foundation';
  7. import { cssClasses, strings, numbers } from '@douyinfe/semi-foundation/select/constants';
  8. import BaseComponent, { ValidateStatus } from '../_base/baseComponent';
  9. import { isEqual, isString, noop, get, isNumber, isFunction } from 'lodash';
  10. import Tag from '../tag/index';
  11. import TagGroup from '../tag/group';
  12. import OverflowList from '../overflowList/index';
  13. import Space from '../space/index';
  14. import Text from '../typography/text';
  15. import LocaleConsumer from '../locale/localeConsumer';
  16. import Popover, { PopoverProps } from '../popover/index';
  17. import { numbers as popoverNumbers } from '@douyinfe/semi-foundation/popover/constants';
  18. import Event from '@douyinfe/semi-foundation/utils/Event';
  19. import { FixedSizeList as List } from 'react-window';
  20. import { getOptionsFromGroup } from './utils';
  21. import VirtualRow from './virtualRow';
  22. import Input, { InputProps } from '../input/index';
  23. import Option, { OptionProps } from './option';
  24. import OptionGroup from './optionGroup';
  25. import Spin from '../spin';
  26. import Trigger from '../trigger';
  27. import { IconChevronDown, IconClear, IconSearch } from '@douyinfe/semi-icons';
  28. import { isSemiIcon, getFocusableElements, getActiveElement, getDefaultPropsFromGlobalConfig } from '../_utils';
  29. import { getUuidShort } from '@douyinfe/semi-foundation/utils/uuid';
  30. import '@douyinfe/semi-foundation/select/select.scss';
  31. import type { Locale } from '../locale/interface';
  32. import type { Position, TooltipProps } from '../tooltip';
  33. import type { Subtract } from 'utility-types';
  34. export type { OptionProps } from './option';
  35. export type { OptionGroupProps } from './optionGroup';
  36. export type { VirtualRowProps } from './virtualRow';
  37. const prefixcls = cssClasses.PREFIX;
  38. const key = 0;
  39. type ExcludeInputType = {
  40. value?: InputProps['value'];
  41. onFocus?: InputProps['onFocus'];
  42. onChange?: InputProps['onChange']
  43. }
  44. type OnChangeValueType = string | number | Record<string, any>;
  45. export interface optionRenderProps {
  46. key?: any;
  47. label?: React.ReactNode;
  48. value?: string | number;
  49. style?: React.CSSProperties;
  50. className?: string;
  51. selected?: boolean;
  52. focused?: boolean;
  53. show?: boolean;
  54. disabled?: boolean;
  55. onMouseEnter?: (e: React.MouseEvent) => any;
  56. onClick?: (e: React.MouseEvent) => any;
  57. [x: string]: any
  58. }
  59. export interface SelectedItemProps {
  60. value: OptionProps['value'];
  61. label: OptionProps['label'];
  62. _show?: boolean;
  63. _selected: boolean;
  64. _scrollIndex?: number
  65. }
  66. export interface TriggerRenderProps {
  67. value: SelectedItemProps[];
  68. inputValue: string;
  69. onSearch: (inputValue: string) => void;
  70. onClear: () => void;
  71. onRemove: (option: OptionProps) => void;
  72. disabled: boolean;
  73. placeholder: string;
  74. componentProps: Record<string, any>
  75. }
  76. export interface selectMethod {
  77. clearInput?: () => void;
  78. selectAll?: () => void;
  79. deselectAll?: () => void;
  80. focus?: () => void;
  81. close?: () => void;
  82. open?: () => void
  83. }
  84. export type SelectSize = 'small' | 'large' | 'default';
  85. export interface virtualListProps {
  86. itemSize?: number;
  87. height?: number;
  88. width?: string | number
  89. }
  90. export type RenderSingleSelectedItemFn = (optionNode: Record<string, any>) => React.ReactNode;
  91. export type RenderMultipleSelectedItemFn = (optionNode: Record<string, any>, multipleProps: { index: number; disabled: boolean; onClose: (tagContent: React.ReactNode, e: MouseEvent) => void }) => { isRenderInTag: boolean; content: React.ReactNode };
  92. export type RenderSelectedItemFn = RenderSingleSelectedItemFn | RenderMultipleSelectedItemFn;
  93. export type SelectProps = {
  94. 'aria-describedby'?: React.AriaAttributes['aria-describedby'];
  95. 'aria-errormessage'?: React.AriaAttributes['aria-errormessage'];
  96. 'aria-invalid'?: React.AriaAttributes['aria-invalid'];
  97. 'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
  98. 'aria-required'?: React.AriaAttributes['aria-required'];
  99. id?: string;
  100. autoFocus?: boolean;
  101. autoClearSearchValue?: boolean;
  102. arrowIcon?: React.ReactNode;
  103. borderless?: boolean;
  104. clearIcon?: React.ReactNode;
  105. defaultValue?: string | number | any[] | Record<string, any>;
  106. value?: string | number | any[] | Record<string, any>;
  107. placeholder?: React.ReactNode;
  108. onChange?: (value: SelectProps['value']) => void;
  109. multiple?: boolean;
  110. filter?: boolean | ((inpueValue: string, option: OptionProps) => boolean);
  111. max?: number;
  112. maxTagCount?: number;
  113. maxHeight?: string | number;
  114. style?: React.CSSProperties;
  115. className?: string;
  116. size?: SelectSize;
  117. disabled?: boolean;
  118. emptyContent?: React.ReactNode;
  119. expandRestTagsOnClick?: boolean;
  120. onDropdownVisibleChange?: (visible: boolean) => void;
  121. zIndex?: number;
  122. position?: Position;
  123. onSearch?: (value: string, event: React.KeyboardEvent | React.MouseEvent) => void;
  124. dropdownClassName?: string;
  125. dropdownStyle?: React.CSSProperties;
  126. dropdownMargin?: PopoverProps['margin'];
  127. ellipsisTrigger?: boolean;
  128. outerTopSlot?: React.ReactNode;
  129. innerTopSlot?: React.ReactNode;
  130. outerBottomSlot?: React.ReactNode;
  131. innerBottomSlot?: React.ReactNode;
  132. optionList?: OptionProps[];
  133. dropdownMatchSelectWidth?: boolean;
  134. loading?: boolean;
  135. defaultOpen?: boolean;
  136. validateStatus?: ValidateStatus;
  137. defaultActiveFirstOption?: boolean;
  138. onChangeWithObject?: boolean;
  139. suffix?: React.ReactNode;
  140. searchPosition?: string;
  141. searchPlaceholder?: string;
  142. prefix?: React.ReactNode;
  143. insetLabel?: React.ReactNode;
  144. insetLabelId?: string;
  145. inputProps?: Subtract<InputProps, ExcludeInputType>;
  146. showClear?: boolean;
  147. showArrow?: boolean;
  148. renderSelectedItem?: RenderSelectedItemFn;
  149. renderCreateItem?: (inputValue: OptionProps['value'], focus: boolean, style?: React.CSSProperties) => React.ReactNode;
  150. renderOptionItem?: (props: optionRenderProps) => React.ReactNode;
  151. onMouseEnter?: (e: React.MouseEvent) => any;
  152. onMouseLeave?: (e: React.MouseEvent) => any;
  153. clickToHide?: boolean;
  154. onExceed?: (option: OptionProps) => void;
  155. onCreate?: (option: OptionProps) => void;
  156. remote?: boolean;
  157. onDeselect?: (value: SelectProps['value'], option: Record<string, any>) => void;
  158. onSelect?: (value: SelectProps['value'], option: Record<string, any>) => void;
  159. allowCreate?: boolean;
  160. triggerRender?: (props: TriggerRenderProps) => React.ReactNode;
  161. onClear?: () => void;
  162. virtualize?: virtualListProps;
  163. onFocus?: (e: React.FocusEvent) => void;
  164. onBlur?: (e: React.FocusEvent) => void;
  165. onListScroll?: (e: React.UIEvent<HTMLDivElement>) => void;
  166. children?: React.ReactNode;
  167. preventScroll?: boolean;
  168. showRestTagsPopover?: boolean;
  169. restTagsPopoverProps?: PopoverProps
  170. } & Pick<
  171. TooltipProps,
  172. | 'spacing'
  173. | 'getPopupContainer'
  174. | 'motion'
  175. | 'autoAdjustOverflow'
  176. | 'mouseLeaveDelay'
  177. | 'mouseEnterDelay'
  178. | 'stopPropagation'
  179. > & React.RefAttributes<any>;
  180. export interface SelectState {
  181. isOpen: boolean;
  182. isFocus: boolean;
  183. options: Array<OptionProps>;
  184. selections: Map<OptionProps['label'], any>; // A collection of all currently selected items, k: label, v: {value,... otherProps}
  185. dropdownMinWidth: number;
  186. optionKey: number;
  187. inputValue: string;
  188. showInput: boolean;
  189. focusIndex: number;
  190. keyboardEventSet: any; // {}
  191. optionGroups: Array<any>;
  192. isHovering: boolean;
  193. isFocusInContainer: boolean;
  194. isFullTags: boolean;
  195. // The number of really-hidden items when maxTagCount is set
  196. overflowItemCount: number
  197. }
  198. // 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
  199. class Select extends BaseComponent<SelectProps, SelectState> {
  200. static contextType = ConfigContext;
  201. static Option = Option;
  202. static OptGroup = OptionGroup;
  203. static propTypes = {
  204. 'aria-describedby': PropTypes.string,
  205. 'aria-errormessage': PropTypes.string,
  206. 'aria-invalid': PropTypes.bool,
  207. 'aria-labelledby': PropTypes.string,
  208. 'aria-required': PropTypes.bool,
  209. autoFocus: PropTypes.bool,
  210. autoClearSearchValue: PropTypes.bool,
  211. borderless: PropTypes.bool,
  212. children: PropTypes.node,
  213. clearIcon: PropTypes.node,
  214. defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array, PropTypes.object]),
  215. ellipsisTrigger: PropTypes.bool,
  216. value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array, PropTypes.object]),
  217. placeholder: PropTypes.node,
  218. onChange: PropTypes.func,
  219. multiple: PropTypes.bool,
  220. // Whether to turn on the input box filtering function, when it is a function, it represents a custom filtering function
  221. filter: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
  222. // How many tags can you choose?
  223. max: PropTypes.number,
  224. // How many tabs are displayed at most, and the rest are displayed in + N
  225. maxTagCount: PropTypes.number,
  226. maxHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  227. style: PropTypes.object,
  228. className: PropTypes.string,
  229. size: PropTypes.oneOf<SelectProps['size']>(strings.SIZE_SET),
  230. disabled: PropTypes.bool,
  231. emptyContent: PropTypes.node,
  232. expandRestTagsOnClick: PropTypes.bool,
  233. onDropdownVisibleChange: PropTypes.func,
  234. zIndex: PropTypes.number,
  235. position: PropTypes.oneOf(strings.POSITION_SET),
  236. onSearch: PropTypes.func,
  237. getPopupContainer: PropTypes.func,
  238. dropdownClassName: PropTypes.string,
  239. dropdownStyle: PropTypes.object,
  240. dropdownMargin: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
  241. outerTopSlot: PropTypes.node,
  242. innerTopSlot: PropTypes.node,
  243. inputProps: PropTypes.object,
  244. outerBottomSlot: PropTypes.node,
  245. innerBottomSlot: PropTypes.node, // Options slot
  246. optionList: PropTypes.array,
  247. dropdownMatchSelectWidth: PropTypes.bool,
  248. loading: PropTypes.bool,
  249. defaultOpen: PropTypes.bool,
  250. validateStatus: PropTypes.oneOf(strings.STATUS),
  251. defaultActiveFirstOption: PropTypes.bool,
  252. triggerRender: PropTypes.func,
  253. stopPropagation: PropTypes.bool,
  254. searchPosition: PropTypes.string,
  255. // motion doesn't need to be exposed
  256. motion: PropTypes.bool,
  257. onChangeWithObject: PropTypes.bool,
  258. suffix: PropTypes.node,
  259. prefix: PropTypes.node,
  260. insetLabel: PropTypes.node,
  261. insetLabelId: PropTypes.string,
  262. showClear: PropTypes.bool,
  263. showArrow: PropTypes.bool,
  264. renderSelectedItem: PropTypes.func,
  265. allowCreate: PropTypes.bool,
  266. renderCreateItem: PropTypes.func,
  267. onMouseEnter: PropTypes.func,
  268. onMouseLeave: PropTypes.func,
  269. clickToHide: PropTypes.bool,
  270. onExceed: PropTypes.func,
  271. onCreate: PropTypes.func,
  272. remote: PropTypes.bool,
  273. onDeselect: PropTypes.func,
  274. // 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
  275. onSelect: PropTypes.func,
  276. autoAdjustOverflow: PropTypes.bool,
  277. mouseEnterDelay: PropTypes.number,
  278. mouseLeaveDelay: PropTypes.number,
  279. spacing: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
  280. onBlur: PropTypes.func,
  281. onFocus: PropTypes.func,
  282. onClear: PropTypes.func,
  283. virtualize: PropTypes.object,
  284. renderOptionItem: PropTypes.func,
  285. onListScroll: PropTypes.func,
  286. arrowIcon: PropTypes.node,
  287. preventScroll: PropTypes.bool,
  288. // open: PropTypes.bool,
  289. // tagClosable: PropTypes.bool,
  290. };
  291. static __SemiComponentName__ = "Select";
  292. static defaultProps: Partial<SelectProps> = getDefaultPropsFromGlobalConfig(Select.__SemiComponentName__, {
  293. stopPropagation: true,
  294. motion: true,
  295. borderless: false,
  296. zIndex: popoverNumbers.DEFAULT_Z_INDEX,
  297. // position: 'bottomLeft',
  298. filter: false,
  299. multiple: false,
  300. disabled: false,
  301. defaultOpen: false,
  302. allowCreate: false,
  303. placeholder: '',
  304. onDropdownVisibleChange: noop,
  305. onChangeWithObject: false,
  306. onChange: noop,
  307. onSearch: noop,
  308. onMouseEnter: noop,
  309. onMouseLeave: noop,
  310. onDeselect: noop,
  311. onSelect: noop,
  312. onCreate: noop,
  313. onExceed: noop,
  314. onFocus: noop,
  315. onBlur: noop,
  316. onClear: noop,
  317. onListScroll: noop,
  318. maxHeight: numbers.LIST_HEIGHT,
  319. dropdownMatchSelectWidth: true,
  320. defaultActiveFirstOption: true, // In order to meet the needs of A11y, change to true
  321. showArrow: true,
  322. showClear: false,
  323. searchPosition: strings.SEARCH_POSITION_TRIGGER,
  324. remote: false,
  325. autoAdjustOverflow: true,
  326. autoClearSearchValue: true,
  327. arrowIcon: <IconChevronDown aria-label='' />,
  328. showRestTagsPopover: false,
  329. restTagsPopoverProps: {},
  330. expandRestTagsOnClick: false,
  331. ellipsisTrigger: false,
  332. // Radio selection is different from the default renderSelectedItem for multiple selection, so it is not declared here
  333. // renderSelectedItem: (optionNode) => optionNode.label,
  334. // The default creator rendering is related to i18, so it is not declared here
  335. // renderCreateItem: (input) => input
  336. })
  337. inputRef: React.RefObject<HTMLInputElement>;
  338. dropdownInputRef: React.RefObject<HTMLInputElement>;
  339. triggerRef: React.RefObject<HTMLDivElement>;
  340. optionContainerEl: React.RefObject<HTMLDivElement>;
  341. optionsRef: React.RefObject<any>;
  342. virtualizeListRef: React.RefObject<any>;
  343. selectOptionListID: string;
  344. selectID: string;
  345. clickOutsideHandler: (e: MouseEvent) => void;
  346. foundation: SelectFoundation;
  347. context: ContextValue;
  348. eventManager: Event;
  349. constructor(props: SelectProps) {
  350. super(props);
  351. this.state = {
  352. isOpen: false,
  353. isFocus: false,
  354. options: [], // All options
  355. selections: new Map(), // A collection of all currently selected items, k: label, v: {value,... otherProps}
  356. dropdownMinWidth: null,
  357. optionKey: key,
  358. inputValue: '',
  359. showInput: false,
  360. focusIndex: props.defaultActiveFirstOption ? 0 : -1,
  361. keyboardEventSet: {},
  362. optionGroups: [],
  363. isHovering: false,
  364. isFocusInContainer: false,
  365. isFullTags: false,
  366. overflowItemCount: 0
  367. };
  368. /* Generate random string */
  369. this.selectOptionListID = '';
  370. this.selectID = '';
  371. this.virtualizeListRef = React.createRef();
  372. this.inputRef = React.createRef();
  373. this.dropdownInputRef = React.createRef(); // only work when searchPosition = 'dropdown'
  374. this.triggerRef = React.createRef();
  375. this.optionsRef = React.createRef();
  376. this.optionContainerEl = React.createRef();
  377. this.clickOutsideHandler = null;
  378. this.onSelect = this.onSelect.bind(this);
  379. this.onClear = this.onClear.bind(this);
  380. this.onMouseEnter = this.onMouseEnter.bind(this);
  381. this.onMouseLeave = this.onMouseLeave.bind(this);
  382. this.renderOption = this.renderOption.bind(this);
  383. this.onKeyPress = this.onKeyPress.bind(this);
  384. this.eventManager = new Event();
  385. this.foundation = new SelectFoundation(this.adapter);
  386. }
  387. setOptionContainerEl = (node: HTMLDivElement) => (this.optionContainerEl = { current: node });
  388. get adapter(): SelectAdapter<SelectProps, SelectState> {
  389. const keyboardAdapter = {
  390. registerKeyDown: (cb: () => void) => {
  391. const keyboardEventSet = {
  392. onKeyDown: cb,
  393. };
  394. this.setState({ keyboardEventSet });
  395. },
  396. unregisterKeyDown: () => {
  397. this.setState({ keyboardEventSet: {} });
  398. },
  399. updateFocusIndex: (focusIndex: number) => {
  400. this.setState({ focusIndex });
  401. },
  402. scrollToFocusOption: () => { },
  403. };
  404. const filterAdapter = {
  405. updateInputValue: (value: string) => {
  406. this.setState({ inputValue: value });
  407. },
  408. toggleInputShow: (showInput: boolean, cb: (...args: any) => void) => {
  409. this.setState({ showInput }, () => {
  410. cb();
  411. });
  412. },
  413. focusInput: () => {
  414. const { preventScroll } = this.props;
  415. if (this.inputRef && this.inputRef.current) {
  416. this.inputRef.current.focus({ preventScroll });
  417. }
  418. },
  419. focusDropdownInput: () => {
  420. const { preventScroll } = this.props;
  421. if (this.dropdownInputRef && this.dropdownInputRef.current) {
  422. this.dropdownInputRef.current.focus({ preventScroll });
  423. }
  424. }
  425. };
  426. const multipleAdapter = {
  427. notifyMaxLimit: (option: OptionProps) => this.props.onExceed(option),
  428. getMaxLimit: () => this.props.max,
  429. registerClickOutsideHandler: (cb: (e: MouseEvent) => void) => {
  430. const clickOutsideHandler: (e: MouseEvent) => void = e => {
  431. const optionInstance = this.optionsRef && this.optionsRef.current;
  432. const triggerDom = (this.triggerRef && this.triggerRef.current) as Element;
  433. const optionsDom = ReactDOM.findDOMNode(optionInstance as ReactInstance);
  434. const target = e.target as Element;
  435. const path = (e as any).composedPath && (e as any).composedPath() || [target];
  436. if (!(optionsDom && optionsDom.contains(target)) &&
  437. !(triggerDom && triggerDom.contains(target)) &&
  438. !(path.includes(triggerDom) || path.includes(optionsDom))
  439. ) {
  440. cb(e);
  441. }
  442. };
  443. this.clickOutsideHandler = clickOutsideHandler;
  444. document.addEventListener('mousedown', clickOutsideHandler as any, false);
  445. },
  446. unregisterClickOutsideHandler: () => {
  447. if (this.clickOutsideHandler) {
  448. document.removeEventListener('mousedown', this.clickOutsideHandler as any, false);
  449. this.clickOutsideHandler = null;
  450. }
  451. },
  452. rePositionDropdown: () => {
  453. let { optionKey } = this.state;
  454. optionKey = optionKey + 1;
  455. this.setState({ optionKey });
  456. },
  457. notifyDeselect: (value: OptionProps['value'], option: OptionProps) => {
  458. delete option._parentGroup;
  459. this.props.onDeselect(value, option);
  460. },
  461. };
  462. return {
  463. ...super.adapter,
  464. ...keyboardAdapter,
  465. ...filterAdapter,
  466. ...multipleAdapter,
  467. on: (eventName, eventCallback) => this.eventManager.on(eventName, eventCallback),
  468. off: (eventName) => this.eventManager.off(eventName),
  469. once: (eventName, eventCallback) => this.eventManager.once(eventName, eventCallback),
  470. emit: (eventName) => this.eventManager.emit(eventName),
  471. // Collect all subitems, each item is visible by default when collected, and is not selected
  472. getOptionsFromChildren: (children = this.props.children) => {
  473. let optionGroups = [];
  474. let options = [];
  475. const { optionList } = this.props;
  476. if (optionList && optionList.length) {
  477. options = optionList.map((itemOpt, index) => ({
  478. _show: true,
  479. _selected: false,
  480. _scrollIndex: index,
  481. ...itemOpt
  482. }));
  483. optionGroups[0] = { children: options, label: '' };
  484. } else {
  485. const result = getOptionsFromGroup(children);
  486. optionGroups = result.optionGroups;
  487. options = result.options;
  488. }
  489. this.setState({ optionGroups });
  490. return options;
  491. },
  492. updateOptions: (options: OptionProps[]) => {
  493. this.setState({ options });
  494. },
  495. openMenu: (cb?: () => void) => {
  496. this.setState({ isOpen: true }, () => {
  497. cb?.();
  498. });
  499. },
  500. closeMenu: () => {
  501. this.setState({ isOpen: false });
  502. },
  503. getTriggerWidth: () => {
  504. const el = this.triggerRef.current;
  505. return el && el.getBoundingClientRect().width;
  506. },
  507. setOptionWrapperWidth: (width: number) => {
  508. this.setState({ dropdownMinWidth: width });
  509. },
  510. updateSelection: (selections: Map<OptionProps['label'], any>) => {
  511. this.setState({ selections });
  512. },
  513. // clone Map, important!!!, prevent unexpected modify on state
  514. getSelections: () => new Map(this.state.selections),
  515. notifyChange: (value: OnChangeValueType | OnChangeValueType[]) => {
  516. this.props.onChange(value);
  517. },
  518. notifySelect: (value: OptionProps['value'], option: OptionProps) => {
  519. delete option._parentGroup;
  520. this.props.onSelect(value, option);
  521. },
  522. notifyDropdownVisibleChange: (visible: boolean) => {
  523. this.props.onDropdownVisibleChange(visible);
  524. },
  525. notifySearch: (input: string, event: React.MouseEvent | React.KeyboardEvent) => {
  526. this.props.onSearch(input, event);
  527. },
  528. notifyCreate: (input: OptionProps) => {
  529. this.props.onCreate(input);
  530. },
  531. notifyMouseEnter: (e: React.MouseEvent<HTMLDivElement>) => {
  532. this.props.onMouseEnter(e);
  533. },
  534. notifyMouseLeave: (e: React.MouseEvent<HTMLDivElement>) => {
  535. this.props.onMouseLeave(e);
  536. },
  537. notifyFocus: (event: React.FocusEvent) => {
  538. this.props.onFocus(event);
  539. },
  540. notifyBlur: (event: React.FocusEvent) => {
  541. this.props.onBlur(event);
  542. },
  543. notifyClear: () => {
  544. this.props.onClear();
  545. },
  546. notifyListScroll: (e: React.UIEvent<HTMLDivElement>) => {
  547. this.props.onListScroll(e);
  548. },
  549. updateHovering: (isHovering: boolean) => {
  550. this.setState({ isHovering });
  551. },
  552. updateFocusState: (isFocus: boolean) => {
  553. this.setState({ isFocus });
  554. },
  555. updateOverflowItemCount: (overflowItemCount: number) => {
  556. this.setState({ overflowItemCount });
  557. },
  558. focusTrigger: () => {
  559. try {
  560. const { preventScroll } = this.props;
  561. const el = (this.triggerRef.current) as any;
  562. el.focus({ preventScroll });
  563. } catch (error) {
  564. }
  565. },
  566. getContainer: () => {
  567. return this.optionContainerEl && this.optionContainerEl.current;
  568. },
  569. getFocusableElements: (node: HTMLDivElement) => {
  570. return getFocusableElements(node);
  571. },
  572. getActiveElement: () => {
  573. return getActiveElement();
  574. },
  575. setIsFocusInContainer: (isFocusInContainer: boolean) => {
  576. this.setState({ isFocusInContainer });
  577. },
  578. getIsFocusInContainer: () => {
  579. return this.state.isFocusInContainer;
  580. },
  581. updateScrollTop: (index?: number) => {
  582. let optionClassName;
  583. if ('renderOptionItem' in this.props) {
  584. optionClassName = `.${prefixcls}-option-custom-selected`;
  585. if (index !== undefined) {
  586. optionClassName = `.${prefixcls}-option-custom:nth-child(${index + 1})`;
  587. }
  588. } else {
  589. optionClassName = `.${prefixcls}-option-selected`;
  590. if (index !== undefined) {
  591. optionClassName = `.${prefixcls}-option:nth-child(${index + 1})`;
  592. }
  593. }
  594. let destNode = document.querySelector(`#${prefixcls}-${this.selectOptionListID} ${optionClassName}`) as HTMLDivElement;
  595. if (Array.isArray(destNode)) {
  596. destNode = destNode[0];
  597. }
  598. if (destNode) {
  599. /**
  600. * Scroll the first selected item into view.
  601. * The reason why ScrollIntoView is not used here is that it may cause page to move.
  602. */
  603. const destParent = destNode.parentNode as HTMLDivElement;
  604. destParent.scrollTop = destNode.offsetTop -
  605. destParent.offsetTop -
  606. (destParent.clientHeight / 2) +
  607. (destNode.clientHeight / 2);
  608. }
  609. },
  610. };
  611. }
  612. componentDidMount() {
  613. this.foundation.init();
  614. this.selectOptionListID = getUuidShort();
  615. this.selectID = this.props.id || getUuidShort();
  616. }
  617. componentWillUnmount() {
  618. this.foundation.destroy();
  619. }
  620. componentDidUpdate(prevProps: SelectProps, prevState: SelectState) {
  621. const prevChildrenKeys = React.Children.toArray(prevProps.children).map((child: any) => child.key);
  622. const nowChildrenKeys = React.Children.toArray(this.props.children).map((child: any) => child.key);
  623. let isOptionsChanged = false;
  624. if (!isEqual(prevChildrenKeys, nowChildrenKeys) || !isEqual(prevProps.optionList, this.props.optionList)) {
  625. isOptionsChanged = true;
  626. this.foundation.handleOptionListChange();
  627. }
  628. // 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
  629. if (!isEqual(this.props.value, prevProps.value) || isOptionsChanged) {
  630. if ('value' in this.props) {
  631. this.foundation.handleValueChange(this.props.value as any);
  632. } else {
  633. this.foundation.handleOptionListChangeHadDefaultValue();
  634. }
  635. }
  636. }
  637. handleInputChange = (value: string, event: React.ChangeEvent<HTMLInputElement>) => this.foundation.handleInputChange(value, event);
  638. renderTriggerInput() {
  639. const { size, multiple, disabled, inputProps, filter } = this.props;
  640. const inputPropsCls = get(inputProps, 'className');
  641. const inputcls = cls(`${prefixcls}-input`, {
  642. [`${prefixcls}-input-single`]: !multiple,
  643. [`${prefixcls}-input-multiple`]: multiple,
  644. }, inputPropsCls);
  645. const { inputValue, focusIndex } = this.state;
  646. const selectInputProps: Record<string, any> = {
  647. value: inputValue,
  648. disabled,
  649. className: inputcls,
  650. onChange: this.handleInputChange,
  651. ...inputProps,
  652. };
  653. let style = {};
  654. // Multiple choice mode
  655. if (multiple) {
  656. style = {
  657. width: inputValue ? `${inputValue.length * 16}px` : '2px',
  658. };
  659. selectInputProps.style = style;
  660. }
  661. return (
  662. <Input
  663. ref={this.inputRef as any}
  664. size={size}
  665. aria-activedescendant={focusIndex !== -1 ? `${this.selectID}-option-${focusIndex}` : ''}
  666. onFocus={(e: React.FocusEvent<HTMLInputElement>) => {
  667. // if multiple and filter, when use tab key to let select get focus
  668. // need to manual update state isFocus to let the focus style take effect
  669. if (multiple && Boolean(filter)) {
  670. this.setState({ isFocus: true });
  671. }
  672. // prevent event bubbling which will fire trigger onFocus event
  673. e.stopPropagation();
  674. // e.nativeEvent.stopImmediatePropagation();
  675. }}
  676. onBlur={e => this.foundation.handleInputBlur(e)}
  677. {...selectInputProps}
  678. />
  679. );
  680. }
  681. renderDropdownInput() {
  682. const { size, multiple, disabled, inputProps, filter, searchPosition, searchPlaceholder } = this.props;
  683. const { inputValue, focusIndex } = this.state;
  684. const wrapperCls = cls(`${prefixcls}-dropdown-search-wrapper`, {
  685. });
  686. const inputPropsCls = get(inputProps, 'className');
  687. const inputCls = cls(`${prefixcls}-dropdown-input`, {
  688. [`${prefixcls}-dropdown-input-single`]: !multiple,
  689. [`${prefixcls}-dropdown-input-multiple`]: multiple,
  690. }, inputPropsCls);
  691. const selectInputProps: Record<string, any> = {
  692. value: inputValue,
  693. disabled,
  694. className: inputCls,
  695. onChange: this.handleInputChange,
  696. placeholder: searchPlaceholder,
  697. showClear: true,
  698. ...inputProps,
  699. /**
  700. * When searchPosition is trigger, the keyboard events are bound to the outer trigger div, so there is no need to listen in input.
  701. * When searchPosition is dropdown, the popup and the outer trigger div are not parent- child relationships,
  702. * and bubbles cannot occur, so onKeydown needs to be listened in input.
  703. * */
  704. onKeyDown: (e) => this.foundation._handleKeyDown(e)
  705. };
  706. return (
  707. <div className={wrapperCls}>
  708. <Input
  709. ref={this.dropdownInputRef}
  710. prefix={<IconSearch></IconSearch>}
  711. aria-activedescendant={focusIndex !== -1 ? `${this.selectID}-option-${focusIndex}` : ''}
  712. {...selectInputProps}
  713. />
  714. </div>
  715. );
  716. }
  717. close() {
  718. this.foundation.close();
  719. }
  720. open() {
  721. this.foundation.open();
  722. }
  723. clearInput() {
  724. this.foundation.clearInput();
  725. }
  726. selectAll() {
  727. this.foundation.selectAll();
  728. }
  729. deselectAll() {
  730. this.foundation.clearSelected();
  731. }
  732. focus() {
  733. this.foundation.focus();
  734. }
  735. onSelect(option: OptionProps, optionIndex: number, e: any) {
  736. this.foundation.onSelect(option, optionIndex, e);
  737. }
  738. onClear(e: React.MouseEvent) {
  739. e.nativeEvent.stopImmediatePropagation();
  740. this.foundation.handleClearClick(e as any);
  741. }
  742. search(value: string, event: React.ChangeEvent<HTMLInputElement>) {
  743. this.handleInputChange(value, event);
  744. }
  745. renderEmpty() {
  746. return <Option empty={true} emptyContent={this.props.emptyContent} />;
  747. }
  748. renderLoading() {
  749. const loadingWrapperCls = `${prefixcls}-loading-wrapper`;
  750. return (
  751. <div className={loadingWrapperCls}>
  752. <Spin />
  753. </div>
  754. );
  755. }
  756. renderOption(option: OptionProps, optionIndex: number, style?: React.CSSProperties) {
  757. const { focusIndex, inputValue } = this.state;
  758. const { renderOptionItem } = this.props;
  759. let optionContent;
  760. const isFocused = optionIndex === focusIndex;
  761. let optionStyle = style || {};
  762. if (option.style) {
  763. optionStyle = { ...optionStyle, ...option.style };
  764. }
  765. if (option._inputCreateOnly) {
  766. optionContent = this.renderCreateOption(option, isFocused, optionIndex, style);
  767. } else {
  768. // use another name to make sure that 'key' in optionList still exist when we call onChange
  769. if ('key' in option) {
  770. option._keyInOptionList = option.key;
  771. }
  772. optionContent = (
  773. <Option
  774. showTick
  775. {...option}
  776. selected={option._selected}
  777. onSelect={(v: OptionProps, e: MouseEvent) => this.onSelect(v, optionIndex, e)}
  778. focused={isFocused}
  779. onMouseEnter={() => this.onOptionHover(optionIndex)}
  780. style={optionStyle}
  781. key={option._keyInOptionList || option._keyInJsx || option.label as string + option.value as string + optionIndex}
  782. renderOptionItem={renderOptionItem}
  783. inputValue={inputValue}
  784. semiOptionId={`${this.selectID}-option-${optionIndex}`}
  785. >
  786. {option.label}
  787. </Option>
  788. );
  789. }
  790. return optionContent;
  791. }
  792. renderCreateOption(option: OptionProps, isFocused: boolean, optionIndex: number, style: React.CSSProperties) {
  793. const { renderCreateItem } = this.props;
  794. // default render method
  795. if (typeof renderCreateItem === 'undefined') {
  796. const defaultCreateItem = (
  797. <Option
  798. key={option.key || option.label as string + option.value as string}
  799. onSelect={(v: OptionProps, e: MouseEvent) => this.onSelect(v, optionIndex, e)}
  800. onMouseEnter={() => this.onOptionHover(optionIndex)}
  801. showTick
  802. {...option}
  803. focused={isFocused}
  804. style={style}
  805. >
  806. <LocaleConsumer<Locale['Select']> componentName="Select" >
  807. {(locale: Locale['Select']) => (
  808. <>
  809. <span className={`${prefixcls}-create-tips`}>{locale.createText}</span>
  810. {option.value}
  811. </>
  812. )}
  813. </LocaleConsumer>
  814. </Option>
  815. );
  816. return defaultCreateItem;
  817. }
  818. const customCreateItem = renderCreateItem(option.value, isFocused, style);
  819. return (
  820. // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/interactive-supports-focus
  821. <div
  822. role="button"
  823. aria-label="Use the input box to create an optional item"
  824. onClick={e => this.onSelect(option, optionIndex, e)}
  825. key={option.key || option.label}
  826. >
  827. {customCreateItem}
  828. </div>
  829. );
  830. }
  831. onOptionHover(optionIndex: number) {
  832. this.foundation.handleOptionMouseEnter(optionIndex);
  833. }
  834. renderWithGroup(visibleOptions: OptionProps[]) {
  835. const content: JSX.Element[] = [];
  836. const groupStatus = new Map();
  837. visibleOptions.forEach((option, optionIndex) => {
  838. const parentGroup = option._parentGroup;
  839. const optionContent = this.renderOption(option, optionIndex);
  840. if (parentGroup && !groupStatus.has(parentGroup.label)) {
  841. const groupKey = typeof parentGroup.label === 'string' || typeof parentGroup.label === 'number'
  842. ? parentGroup.label
  843. : parentGroup.key;
  844. // when use with OptionGroup and group content not already insert
  845. const groupContent = <OptionGroup {...parentGroup} key={groupKey}/>;
  846. groupStatus.set(parentGroup.label, true);
  847. content.push(groupContent);
  848. }
  849. content.push(optionContent);
  850. });
  851. return content;
  852. }
  853. renderVirtualizeList(visibleOptions: OptionProps[]) {
  854. const { virtualize } = this.props;
  855. const { direction } = this.context;
  856. const { height, width, itemSize } = virtualize;
  857. return (
  858. <List
  859. ref={this.virtualizeListRef}
  860. height={height || numbers.LIST_HEIGHT}
  861. itemCount={visibleOptions.length}
  862. itemSize={itemSize}
  863. itemData={{ visibleOptions, renderOption: this.renderOption }}
  864. width={width || '100%'}
  865. style={{ direction }}
  866. >
  867. {VirtualRow}
  868. </List>
  869. );
  870. }
  871. renderOptions(children?: React.ReactNode) {
  872. const { dropdownMinWidth, options, selections } = this.state;
  873. const {
  874. maxHeight,
  875. dropdownClassName,
  876. dropdownStyle,
  877. outerTopSlot,
  878. innerTopSlot,
  879. outerBottomSlot,
  880. innerBottomSlot,
  881. loading,
  882. virtualize,
  883. multiple,
  884. emptyContent,
  885. searchPosition,
  886. filter,
  887. } = this.props;
  888. // Do a filter first, instead of directly judging in forEach, so that the focusIndex can correspond to
  889. const visibleOptions = options.filter(item => item._show);
  890. let listContent: JSX.Element | JSX.Element[] = this.renderWithGroup(visibleOptions);
  891. if (virtualize) {
  892. listContent = this.renderVirtualizeList(visibleOptions);
  893. }
  894. const style = { minWidth: dropdownMinWidth, ...dropdownStyle };
  895. const optionListCls = cls({
  896. [`${prefixcls}-option-list`]: true,
  897. [`${prefixcls}-option-list-chosen`]: selections.size,
  898. });
  899. const isEmpty = !options.length || !options.some(item => item._show);
  900. return (
  901. // eslint-disable-next-line jsx-a11y/no-static-element-interactions
  902. <div
  903. id={`${prefixcls}-${this.selectOptionListID}`}
  904. className={cls({
  905. // When emptyContent is null and the option is empty, there is no need for the drop-down option for the user,
  906. // so there is no need to set padding through this className
  907. [`${prefixcls}-option-list-wrapper`]: !(isEmpty && emptyContent === null),
  908. }, dropdownClassName)}
  909. style={style}
  910. ref={this.setOptionContainerEl}
  911. onKeyDown={e => this.foundation.handleContainerKeyDown(e)}
  912. >
  913. {outerTopSlot ? <div className={`${prefixcls}-option-list-outer-top-slot`} onMouseEnter={() => this.foundation.handleSlotMouseEnter()}>{outerTopSlot}</div> : null}
  914. {searchPosition === strings.SEARCH_POSITION_DROPDOWN && filter ? this.renderDropdownInput() : null}
  915. <div
  916. style={{ maxHeight: `${maxHeight}px` }}
  917. className={optionListCls}
  918. role="listbox"
  919. aria-multiselectable={multiple}
  920. onScroll={e => this.foundation.handleListScroll(e)}
  921. >
  922. {innerTopSlot ? <div className={`${prefixcls}-option-list-inner-top-slot`} onMouseEnter={() => this.foundation.handleSlotMouseEnter()}>{innerTopSlot}</div> : null}
  923. {loading ? this.renderLoading() : isEmpty ? this.renderEmpty() : listContent}
  924. {innerBottomSlot ? <div className={`${prefixcls}-option-list-inner-bottom-slot`} onMouseEnter={() => this.foundation.handleSlotMouseEnter()}>{innerBottomSlot}</div> : null}
  925. </div>
  926. {outerBottomSlot ? <div className={`${prefixcls}-option-list-outer-bottom-slot`} onMouseEnter={() => this.foundation.handleSlotMouseEnter()}>{outerBottomSlot}</div> : null}
  927. </div>
  928. );
  929. }
  930. renderSingleSelection(selections: Map<OptionProps['label'], any>, filterable: boolean) {
  931. let { renderSelectedItem, searchPosition } = this.props;
  932. const { placeholder } = this.props;
  933. const { showInput, inputValue } = this.state;
  934. let renderText: React.ReactNode = '';
  935. const selectedItems = [...selections];
  936. if (typeof renderSelectedItem === 'undefined') {
  937. renderSelectedItem = ((optionNode: OptionProps) => optionNode.label) as RenderSelectedItemFn;
  938. }
  939. if (selectedItems.length) {
  940. const selectedItem = selectedItems[0][1];
  941. renderText = (renderSelectedItem as RenderSingleSelectedItemFn)(selectedItem);
  942. }
  943. const showInputInTrigger = searchPosition === strings.SEARCH_POSITION_TRIGGER;
  944. const spanCls = cls({
  945. [`${prefixcls}-selection-text`]: true,
  946. [`${prefixcls}-selection-placeholder`]: !renderText && renderText !== 0,
  947. [`${prefixcls}-selection-text-hide`]: inputValue && showInput && showInputInTrigger, // show Input
  948. [`${prefixcls}-selection-text-inactive`]: !inputValue && showInput && showInputInTrigger, // Stack Input & RenderText(opacity 0.4)
  949. });
  950. const contentWrapperCls = `${prefixcls}-content-wrapper`;
  951. return (
  952. <>
  953. <div className={contentWrapperCls}>
  954. {
  955. <span className={spanCls} x-semi-prop="placeholder">
  956. {renderText || renderText === 0 ? renderText : placeholder}
  957. </span>
  958. }
  959. {filterable && showInput && showInputInTrigger ? this.renderTriggerInput() : null}
  960. </div>
  961. </>
  962. );
  963. }
  964. getTagItem = (item: any, i: number, renderSelectedItem: RenderSelectedItemFn) => {
  965. const { size, disabled: selectDisabled } = this.props;
  966. const label = item[0];
  967. const { value } = item[1];
  968. const disabled = item[1].disabled || selectDisabled;
  969. const onClose = (tagContent: React.ReactNode, e: MouseEvent) => {
  970. if (e && typeof e.preventDefault === 'function') {
  971. e.preventDefault(); // make sure that tag will not hidden immediately in controlled mode
  972. }
  973. this.foundation.removeTag({ label, value });
  974. };
  975. const { content, isRenderInTag } = (renderSelectedItem as RenderMultipleSelectedItemFn)(item[1], { index: i, disabled, onClose });
  976. const basic = {
  977. disabled,
  978. closable: !disabled,
  979. onClose,
  980. };
  981. if (isRenderInTag) {
  982. return (
  983. <Tag {...basic} color="white" size={size || 'large'} key={value} tabIndex={-1}>
  984. {content}
  985. </Tag>
  986. );
  987. } else {
  988. return <Fragment key={value}>{content}</Fragment>;
  989. }
  990. }
  991. renderTag(item: [React.ReactNode, any], i: number, isCollapseItem?: boolean) {
  992. const { size, disabled: selectDisabled } = this.props;
  993. let { renderSelectedItem } = this.props;
  994. const label = item[0];
  995. const { value } = item[1];
  996. const disabled = item[1].disabled || selectDisabled;
  997. const onClose = (tagContent: React.ReactNode, e: MouseEvent) => {
  998. if (e && typeof e.preventDefault === 'function') {
  999. e.preventDefault(); // make sure that tag will not hidden immediately in controlled mode
  1000. }
  1001. this.foundation.removeTag({ label, value });
  1002. };
  1003. if (typeof renderSelectedItem === 'undefined') {
  1004. renderSelectedItem = (optionNode: OptionProps) => ({
  1005. isRenderInTag: true,
  1006. content: optionNode.label,
  1007. });
  1008. }
  1009. const { content, isRenderInTag } = (renderSelectedItem as RenderMultipleSelectedItemFn)(item[1], { index: i, disabled, onClose });
  1010. const basic = {
  1011. disabled,
  1012. closable: !disabled,
  1013. onClose,
  1014. };
  1015. const realContent = isCollapseItem && !isFunction(this.props.renderSelectedItem)
  1016. ? (
  1017. <Text size='small' ellipsis={{ rows: 1, showTooltip: { type: 'popover', opts: { style: { width: 'auto', fontSize: 12 } } } }} >
  1018. {content}
  1019. </Text>
  1020. )
  1021. : content;
  1022. if (isRenderInTag) {
  1023. return (
  1024. <Tag {...basic} color="white" size={size || 'large'} key={value} style={{ maxWidth: '100%' }}>
  1025. {realContent}
  1026. </Tag>
  1027. );
  1028. } else {
  1029. return <Fragment key={value}>{realContent}</Fragment>;
  1030. }
  1031. }
  1032. renderNTag(n: number, restTags: [React.ReactNode, any][]) {
  1033. const { size, showRestTagsPopover, restTagsPopoverProps } = this.props;
  1034. let nTag = (
  1035. <Tag
  1036. closable={false}
  1037. size={size || 'large'}
  1038. color='grey'
  1039. className={`${prefixcls}-content-wrapper-collapse-tag`}
  1040. key={`_+${n}`}
  1041. style={{ marginRight: 0, flexShrink: 0 }}
  1042. >
  1043. +{n}
  1044. </Tag>
  1045. );
  1046. if (showRestTagsPopover) {
  1047. nTag = (
  1048. <Popover
  1049. showArrow
  1050. content={
  1051. <Space spacing={2} wrap style={{ maxWidth: '400px' }}>
  1052. {restTags.map((tag, index) => (this.renderTag(tag, index)))}
  1053. </Space>
  1054. }
  1055. trigger="hover"
  1056. position="top"
  1057. autoAdjustOverflow
  1058. {...restTagsPopoverProps}
  1059. key={`_+${n}_Popover`}
  1060. >
  1061. {nTag}
  1062. </Popover>
  1063. );
  1064. }
  1065. return nTag;
  1066. }
  1067. renderOverflow(items: [React.ReactNode, any][], index: number) {
  1068. const isCollapse = true;
  1069. return items.length && items[0]
  1070. ? this.renderTag(items[0], index, isCollapse)
  1071. : null;
  1072. }
  1073. handleOverflow(items: [React.ReactNode, any][]) {
  1074. const { overflowItemCount, selections } = this.state;
  1075. const { maxTagCount } = this.props;
  1076. const newOverFlowItemCount = selections.size - maxTagCount > 0 ? selections.size - maxTagCount + items.length - 1 : items.length - 1;
  1077. if (overflowItemCount !== newOverFlowItemCount) {
  1078. this.foundation.updateOverflowItemCount(selections.size, newOverFlowItemCount);
  1079. }
  1080. }
  1081. renderCollapsedTags(selections: [React.ReactNode, any][], length: number | undefined): React.ReactElement {
  1082. const { overflowItemCount } = this.state;
  1083. const normalTags = typeof length === 'number' ? selections.slice(0, length) : selections;
  1084. return (
  1085. <div className={`${prefixcls}-content-wrapper-collapse`}>
  1086. <OverflowList
  1087. items={normalTags}
  1088. key={String(selections.length)}
  1089. overflowRenderer={overflowItems => this.renderOverflow(overflowItems as [React.ReactNode, any][], length - 1)}
  1090. onOverflow={overflowItems => this.handleOverflow(overflowItems as [React.ReactNode, any][])}
  1091. visibleItemRenderer={(item, index) => this.renderTag(item as [React.ReactNode, any], index)}
  1092. />
  1093. {overflowItemCount > 0 && this.renderNTag(overflowItemCount, selections.slice(selections.length - overflowItemCount))}
  1094. </div>
  1095. );
  1096. }
  1097. renderOneLineTags(selectedItems: [React.ReactNode, any][], n: number | undefined): React.ReactElement {
  1098. let { renderSelectedItem } = this.props;
  1099. const { showRestTagsPopover, restTagsPopoverProps, maxTagCount } = this.props;
  1100. const { isFullTags } = this.state;
  1101. let tagContent: ReactNode;
  1102. if (typeof renderSelectedItem === 'undefined') {
  1103. renderSelectedItem = (optionNode: OptionProps) => ({
  1104. isRenderInTag: true,
  1105. content: optionNode.label,
  1106. });
  1107. }
  1108. if (showRestTagsPopover) {
  1109. // showRestTagsPopover = true,
  1110. const mapItems = isFullTags ? selectedItems : selectedItems.slice(0, maxTagCount);
  1111. const tags = mapItems.map((item, i) => {
  1112. return this.getTagItem(item, i, renderSelectedItem);
  1113. });
  1114. tagContent = (
  1115. <TagGroup<"custom">
  1116. tagList={tags}
  1117. maxTagCount={n}
  1118. restCount={isFullTags ? undefined : (selectedItems.length - maxTagCount)}
  1119. size="large"
  1120. mode="custom"
  1121. showPopover={showRestTagsPopover}
  1122. popoverProps={restTagsPopoverProps}
  1123. onPlusNMouseEnter={() => {
  1124. this.foundation.updateIsFullTags();
  1125. }}
  1126. />
  1127. );
  1128. } else {
  1129. // If maxTagCount is set, showRestTagsPopover is false/undefined,
  1130. // then there is no popover when hovering, no extra Tags are displayed,
  1131. // only the tags and restCount displayed in the trigger need to be passed in
  1132. const mapItems = selectedItems.slice(0, maxTagCount);
  1133. const tags = mapItems.map((item, i) => {
  1134. return this.getTagItem(item, i, renderSelectedItem);
  1135. });
  1136. tagContent = (
  1137. <TagGroup<"custom">
  1138. tagList={tags}
  1139. maxTagCount={n}
  1140. restCount={selectedItems.length - maxTagCount}
  1141. size="large"
  1142. mode="custom"
  1143. />
  1144. );
  1145. }
  1146. return tagContent;
  1147. }
  1148. renderMultipleSelection(selections: Map<OptionProps['label'], any>, filterable: boolean) {
  1149. let { renderSelectedItem, searchPosition } = this.props;
  1150. const { placeholder, maxTagCount, expandRestTagsOnClick, ellipsisTrigger } = this.props;
  1151. const { inputValue, isOpen } = this.state;
  1152. const selectedItems = [...selections];
  1153. if (typeof renderSelectedItem === 'undefined') {
  1154. renderSelectedItem = (optionNode: OptionProps) => ({
  1155. isRenderInTag: true,
  1156. content: optionNode.label,
  1157. });
  1158. }
  1159. const contentWrapperCls = cls({
  1160. [`${prefixcls}-content-wrapper`]: true,
  1161. [`${prefixcls}-content-wrapper-one-line`]: maxTagCount && !isOpen,
  1162. [`${prefixcls}-content-wrapper-empty`]: !selectedItems.length,
  1163. });
  1164. const spanCls = cls({
  1165. [`${prefixcls}-selection-text`]: true,
  1166. [`${prefixcls}-selection-placeholder`]: !selectedItems.length,
  1167. [`${prefixcls}-selection-text-hide`]: selectedItems && selectedItems.length,
  1168. });
  1169. const placeholderText = placeholder && !inputValue ? <span className={spanCls}>{placeholder}</span> : null;
  1170. const n = selectedItems.length > maxTagCount ? maxTagCount : undefined;
  1171. const NotOneLine = !maxTagCount;
  1172. const oneLineTags = ellipsisTrigger ? this.renderCollapsedTags(selectedItems, n) : this.renderOneLineTags(selectedItems, n);
  1173. const tagContent = NotOneLine || (expandRestTagsOnClick && isOpen)
  1174. ? selectedItems.map((item, i) => this.renderTag(item, i))
  1175. : oneLineTags;
  1176. const showTriggerInput = filterable && searchPosition === strings.SEARCH_POSITION_TRIGGER;
  1177. return (
  1178. <>
  1179. <div className={contentWrapperCls}>
  1180. {selectedItems && selectedItems.length ? tagContent : placeholderText}
  1181. {showTriggerInput ? this.renderTriggerInput() : null}
  1182. </div>
  1183. </>
  1184. );
  1185. }
  1186. onMouseEnter(e: MouseEvent) {
  1187. this.foundation.handleMouseEnter(e as any);
  1188. }
  1189. onMouseLeave(e: MouseEvent) {
  1190. this.foundation.handleMouseLeave(e as any);
  1191. }
  1192. onKeyPress(e: React.KeyboardEvent) {
  1193. this.foundation.handleKeyPress(e as any);
  1194. }
  1195. /* Processing logic when popover visible changes */
  1196. handlePopoverVisibleChange(status) {
  1197. const { virtualize } = this.props;
  1198. const { selections } = this.state;
  1199. if (!status) {
  1200. return;
  1201. }
  1202. if (virtualize) {
  1203. let minItemIndex = -1;
  1204. selections.forEach(item => {
  1205. const itemIndex = get(item, '_scrollIndex');
  1206. /* When the itemIndex is legal */
  1207. if (isNumber(itemIndex) && itemIndex >= 0) {
  1208. minItemIndex = minItemIndex !== -1 && minItemIndex < itemIndex
  1209. ? minItemIndex
  1210. : itemIndex;
  1211. }
  1212. });
  1213. if (minItemIndex !== -1) {
  1214. try {
  1215. this.virtualizeListRef.current.scrollToItem(minItemIndex, 'center');
  1216. } catch (error) { }
  1217. }
  1218. } else {
  1219. this.foundation.updateScrollTop();
  1220. }
  1221. }
  1222. renderSuffix() {
  1223. const { suffix } = this.props;
  1224. const suffixWrapperCls = cls({
  1225. [`${prefixcls}-suffix`]: true,
  1226. [`${prefixcls}-suffix-text`]: suffix && isString(suffix),
  1227. [`${prefixcls}-suffix-icon`]: isSemiIcon(suffix),
  1228. });
  1229. return <div className={suffixWrapperCls} x-semi-prop="suffix">{suffix}</div>;
  1230. }
  1231. renderPrefix() {
  1232. const { prefix, insetLabel, insetLabelId } = this.props;
  1233. const labelNode = (prefix || insetLabel) as React.ReactElement<any, any>;
  1234. const prefixWrapperCls = cls({
  1235. [`${prefixcls}-prefix`]: true,
  1236. [`${prefixcls}-inset-label`]: insetLabel,
  1237. [`${prefixcls}-prefix-text`]: labelNode && isString(labelNode),
  1238. [`${prefixcls}-prefix-icon`]: isSemiIcon(labelNode),
  1239. });
  1240. return (
  1241. <div className={prefixWrapperCls} id={insetLabelId} x-semi-prop="prefix,insetLabel">
  1242. {labelNode}
  1243. </div>
  1244. );
  1245. }
  1246. renderSelection() {
  1247. const {
  1248. disabled,
  1249. multiple,
  1250. filter,
  1251. style,
  1252. id,
  1253. size,
  1254. className,
  1255. validateStatus,
  1256. showArrow,
  1257. suffix,
  1258. prefix,
  1259. insetLabel,
  1260. placeholder,
  1261. triggerRender,
  1262. arrowIcon,
  1263. clearIcon,
  1264. borderless,
  1265. ...rest
  1266. } = this.props;
  1267. const { selections, isOpen, keyboardEventSet, inputValue, isHovering, isFocus, showInput, focusIndex } = this.state;
  1268. const useCustomTrigger = typeof triggerRender === 'function';
  1269. const filterable = Boolean(filter); // filter(boolean || function)
  1270. const selectionCls = useCustomTrigger ?
  1271. cls(className) :
  1272. cls(prefixcls, className, {
  1273. [`${prefixcls}-borderless`]: borderless,
  1274. [`${prefixcls}-open`]: isOpen,
  1275. [`${prefixcls}-focus`]: isFocus,
  1276. [`${prefixcls}-disabled`]: disabled,
  1277. [`${prefixcls}-single`]: !multiple,
  1278. [`${prefixcls}-multiple`]: multiple,
  1279. [`${prefixcls}-filterable`]: filterable,
  1280. [`${prefixcls}-small`]: size === 'small',
  1281. [`${prefixcls}-large`]: size === 'large',
  1282. [`${prefixcls}-error`]: validateStatus === 'error',
  1283. [`${prefixcls}-warning`]: validateStatus === 'warning',
  1284. [`${prefixcls}-no-arrow`]: !showArrow,
  1285. [`${prefixcls}-with-prefix`]: prefix || insetLabel,
  1286. [`${prefixcls}-with-suffix`]: suffix,
  1287. });
  1288. const showClear = this.props.showClear &&
  1289. (selections.size || inputValue) && !disabled && (isHovering || isOpen);
  1290. const arrowContent = showArrow ? (
  1291. <div className={`${prefixcls}-arrow`} x-semi-prop="arrowIcon">
  1292. {arrowIcon}
  1293. </div>
  1294. ) : (
  1295. <div className={`${prefixcls}-arrow-empty`} />
  1296. );
  1297. const clear = clearIcon ? clearIcon : <IconClear />;
  1298. // semantics of onSearch are more in line with behavior, onChange is alias of onSearch, will be deprecate next major version
  1299. const inner = useCustomTrigger ? (
  1300. <Trigger
  1301. value={Array.from(selections.values())}
  1302. inputValue={inputValue}
  1303. onChange={this.handleInputChange}
  1304. onSearch={this.handleInputChange}
  1305. onRemove={(item) => this.foundation.removeTag(item)}
  1306. onClear={this.onClear}
  1307. disabled={disabled}
  1308. triggerRender={triggerRender}
  1309. placeholder={placeholder as any}
  1310. componentName="Select"
  1311. componentProps={{ ...this.props }}
  1312. />
  1313. ) : (
  1314. [
  1315. <Fragment key="prefix">{prefix || insetLabel ? this.renderPrefix() : null}</Fragment>,
  1316. <Fragment key="selection">
  1317. <div className={cls(`${prefixcls}-selection`)}>
  1318. {multiple ?
  1319. this.renderMultipleSelection(selections, filterable) :
  1320. this.renderSingleSelection(selections, filterable)}
  1321. </div>
  1322. </Fragment>,
  1323. <Fragment key="suffix">{suffix ? this.renderSuffix() : null}</Fragment>,
  1324. <Fragment key="clearicon">
  1325. {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
  1326. {showClear ? (<div className={cls(`${prefixcls}-clear`)} onClick={this.onClear}>{clear}</div>) : arrowContent}
  1327. </Fragment>,
  1328. ]
  1329. );
  1330. /**
  1331. *
  1332. * In disabled, searchable single-selection and display input, and searchable multi-selection
  1333. * make combobox not focusable by tab key
  1334. *
  1335. * 在disabled,可搜索单选且显示input框,以及可搜索多选情况下
  1336. * 让combobox无法通过tab聚焦
  1337. */
  1338. const tabIndex = (disabled || (filterable && showInput) || (filterable && multiple)) ? -1 : 0;
  1339. return (
  1340. /* eslint-disable-next-line jsx-a11y/aria-activedescendant-has-tabindex */
  1341. <div
  1342. role="combobox"
  1343. aria-disabled={disabled}
  1344. aria-expanded={isOpen}
  1345. aria-controls={`${prefixcls}-${this.selectOptionListID}`}
  1346. aria-haspopup="listbox"
  1347. aria-label={selections.size ? 'selected' : ''} // if there is a value, expect the narration to speak selected
  1348. aria-invalid={this.props['aria-invalid']}
  1349. aria-errormessage={this.props['aria-errormessage']}
  1350. aria-labelledby={this.props['aria-labelledby']}
  1351. aria-describedby={this.props['aria-describedby']}
  1352. aria-required={this.props['aria-required']}
  1353. className={selectionCls}
  1354. ref={ref => ((this.triggerRef as any).current = ref)}
  1355. onClick={e => this.foundation.handleClick(e)}
  1356. style={style}
  1357. id={this.selectID}
  1358. tabIndex={tabIndex}
  1359. aria-activedescendant={focusIndex !== -1 ? `${this.selectID}-option-${focusIndex}` : ''}
  1360. onMouseEnter={this.onMouseEnter}
  1361. onMouseLeave={this.onMouseLeave}
  1362. onFocus={e => this.foundation.handleTriggerFocus(e)}
  1363. onBlur={e => this.foundation.handleTriggerBlur(e as any)}
  1364. onKeyPress={this.onKeyPress}
  1365. {...keyboardEventSet}
  1366. {...this.getDataAttr(rest)}
  1367. >
  1368. {inner}
  1369. </div>
  1370. );
  1371. }
  1372. render() {
  1373. const { direction } = this.context;
  1374. const defaultPosition = direction === 'rtl' ? 'bottomRight' : 'bottomLeft';
  1375. const {
  1376. children,
  1377. position = defaultPosition,
  1378. zIndex,
  1379. getPopupContainer,
  1380. motion,
  1381. autoAdjustOverflow,
  1382. mouseLeaveDelay,
  1383. mouseEnterDelay,
  1384. spacing,
  1385. stopPropagation,
  1386. dropdownMargin,
  1387. } = this.props;
  1388. const { isOpen, optionKey } = this.state;
  1389. const selection = this.renderSelection();
  1390. return (
  1391. <Popover
  1392. getPopupContainer={getPopupContainer}
  1393. motion={motion}
  1394. margin={dropdownMargin}
  1395. autoAdjustOverflow={autoAdjustOverflow}
  1396. mouseLeaveDelay={mouseLeaveDelay}
  1397. mouseEnterDelay={mouseEnterDelay}
  1398. zIndex={zIndex}
  1399. ref={this.optionsRef}
  1400. content={() => this.renderOptions(children)}
  1401. visible={isOpen}
  1402. trigger="custom"
  1403. rePosKey={optionKey}
  1404. position={position}
  1405. spacing={spacing}
  1406. stopPropagation={stopPropagation}
  1407. disableArrowKeyDown={true}
  1408. onVisibleChange={status => this.handlePopoverVisibleChange(status)}
  1409. afterClose={() => this.foundation.handlePopoverClose()}
  1410. >
  1411. {selection}
  1412. </Popover>
  1413. );
  1414. }
  1415. }
  1416. export default Select;