index.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644
  1. import React from 'react';
  2. import cls from 'classnames';
  3. import PropTypes from 'prop-types';
  4. import {
  5. noop,
  6. isString,
  7. isArray,
  8. isNull,
  9. isUndefined,
  10. isFunction
  11. } from 'lodash';
  12. import { cssClasses, strings } from '@douyinfe/semi-foundation/tagInput/constants';
  13. import '@douyinfe/semi-foundation/tagInput/tagInput.scss';
  14. import TagInputFoundation, { TagInputAdapter, OnSortEndProps } from '@douyinfe/semi-foundation/tagInput/foundation';
  15. import { ArrayElement } from '../_base/base';
  16. import { isSemiIcon } from '../_utils';
  17. import BaseComponent from '../_base/baseComponent';
  18. import Tag from '../tag';
  19. import Input from '../input';
  20. import Popover, { PopoverProps } from '../popover';
  21. import Paragraph from '../typography/paragraph';
  22. import { IconClear, IconHandle } from '@douyinfe/semi-icons';
  23. import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
  24. import { ShowTooltip } from '../typography';
  25. const prefixCls = cssClasses.PREFIX;
  26. export type Size = ArrayElement<typeof strings.SIZE_SET>;
  27. export type RestTagsPopoverProps = PopoverProps;
  28. type ValidateStatus = "default" | "error" | "warning";
  29. const SortableItem = SortableElement(props => props.item);
  30. const SortableList = SortableContainer(
  31. ({ items }) => {
  32. return (
  33. <div className={`${prefixCls}-sortable-list`}>
  34. {items.map((item, index) => (
  35. // @ts-ignore skip SortableItem type check
  36. <SortableItem key={item.key} index={index} item={item.item}></SortableItem>
  37. ))}
  38. </div>
  39. );
  40. });
  41. export interface TagInputProps {
  42. className?: string;
  43. clearIcon?: React.ReactNode;
  44. defaultValue?: string[];
  45. disabled?: boolean;
  46. inputValue?: string;
  47. maxLength?: number;
  48. max?: number;
  49. maxTagCount?: number;
  50. showRestTagsPopover?: boolean;
  51. restTagsPopoverProps?: RestTagsPopoverProps;
  52. showContentTooltip?: boolean | ShowTooltip;
  53. allowDuplicates?: boolean;
  54. addOnBlur?: boolean;
  55. draggable?: boolean;
  56. expandRestTagsOnClick?: boolean;
  57. onAdd?: (addedValue: string[]) => void;
  58. onBlur?: (e: React.MouseEvent<HTMLInputElement>) => void;
  59. onChange?: (value: string[]) => void;
  60. onExceed?: ((value: string[]) => void);
  61. onFocus?: (e: React.MouseEvent<HTMLInputElement>) => void;
  62. onInputChange?: (value: string, e: React.MouseEvent<HTMLInputElement>) => void;
  63. onInputExceed?: ((value: string) => void);
  64. onKeyDown?: (e: React.MouseEvent<HTMLInputElement>) => void;
  65. onRemove?: (removedValue: string, idx: number) => void;
  66. placeholder?: string;
  67. insetLabel?: React.ReactNode;
  68. insetLabelId?: string;
  69. prefix?: React.ReactNode;
  70. renderTagItem?: (value: string, index: number, onClose: () => void) => React.ReactNode;
  71. separator?: string | string[] | null;
  72. showClear?: boolean;
  73. size?: Size;
  74. style?: React.CSSProperties;
  75. suffix?: React.ReactNode;
  76. validateStatus?: ValidateStatus;
  77. value?: string[] | undefined;
  78. autoFocus?: boolean;
  79. 'aria-label'?: string;
  80. preventScroll?: boolean
  81. }
  82. export interface TagInputState {
  83. tagsArray?: string[];
  84. inputValue?: string;
  85. focusing?: boolean;
  86. hovering?: boolean;
  87. active?: boolean;
  88. // entering: Used to identify whether the user is in a new composition session(eg,Input Chinese)
  89. entering?: boolean
  90. }
  91. class TagInput extends BaseComponent<TagInputProps, TagInputState> {
  92. static propTypes = {
  93. children: PropTypes.node,
  94. clearIcon: PropTypes.node,
  95. style: PropTypes.object,
  96. className: PropTypes.string,
  97. disabled: PropTypes.bool,
  98. allowDuplicates: PropTypes.bool,
  99. max: PropTypes.number,
  100. maxTagCount: PropTypes.number,
  101. maxLength: PropTypes.number,
  102. showRestTagsPopover: PropTypes.bool,
  103. restTagsPopoverProps: PropTypes.object,
  104. showContentTooltip: PropTypes.oneOfType([
  105. PropTypes.shape({
  106. type: PropTypes.string,
  107. opts: PropTypes.object,
  108. }),
  109. PropTypes.bool,
  110. ]),
  111. defaultValue: PropTypes.array,
  112. value: PropTypes.array,
  113. inputValue: PropTypes.string,
  114. placeholder: PropTypes.string,
  115. separator: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
  116. showClear: PropTypes.bool,
  117. addOnBlur: PropTypes.bool,
  118. draggable: PropTypes.bool,
  119. expandRestTagsOnClick: PropTypes.bool,
  120. autoFocus: PropTypes.bool,
  121. renderTagItem: PropTypes.func,
  122. onBlur: PropTypes.func,
  123. onFocus: PropTypes.func,
  124. onChange: PropTypes.func,
  125. onInputChange: PropTypes.func,
  126. onExceed: PropTypes.func,
  127. onInputExceed: PropTypes.func,
  128. onAdd: PropTypes.func,
  129. onRemove: PropTypes.func,
  130. onKeyDown: PropTypes.func,
  131. size: PropTypes.oneOf(strings.SIZE_SET),
  132. validateStatus: PropTypes.oneOf(strings.STATUS),
  133. prefix: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
  134. suffix: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
  135. 'aria-label': PropTypes.string,
  136. preventScroll: PropTypes.bool,
  137. };
  138. static defaultProps = {
  139. showClear: false,
  140. addOnBlur: false,
  141. allowDuplicates: true,
  142. showRestTagsPopover: true,
  143. autoFocus: false,
  144. draggable: false,
  145. expandRestTagsOnClick: true,
  146. showContentTooltip: true,
  147. separator: ',',
  148. size: 'default' as const,
  149. validateStatus: 'default' as const,
  150. onBlur: noop,
  151. onFocus: noop,
  152. onChange: noop,
  153. onInputChange: noop,
  154. onExceed: noop,
  155. onInputExceed: noop,
  156. onAdd: noop,
  157. onRemove: noop,
  158. onKeyDown: noop,
  159. };
  160. inputRef: React.RefObject<HTMLInputElement>;
  161. tagInputRef: React.RefObject<HTMLDivElement>;
  162. foundation: TagInputFoundation;
  163. clickOutsideHandler: any;
  164. constructor(props: TagInputProps) {
  165. super(props);
  166. this.foundation = new TagInputFoundation(this.adapter);
  167. this.state = {
  168. tagsArray: props.defaultValue || [],
  169. inputValue: '',
  170. focusing: false,
  171. hovering: false,
  172. active: false,
  173. entering: false,
  174. };
  175. this.inputRef = React.createRef();
  176. this.tagInputRef = React.createRef();
  177. this.clickOutsideHandler = null;
  178. }
  179. static getDerivedStateFromProps(nextProps: TagInputProps, prevState: TagInputState) {
  180. const { value, inputValue } = nextProps;
  181. const { tagsArray: prevTagsArray } = prevState;
  182. let tagsArray: string[];
  183. if (isArray(value)) {
  184. tagsArray = value;
  185. } else if ('value' in nextProps && !value) {
  186. tagsArray = [];
  187. } else {
  188. tagsArray = prevTagsArray;
  189. }
  190. return {
  191. tagsArray,
  192. inputValue: isString(inputValue) ? inputValue : prevState.inputValue
  193. };
  194. }
  195. get adapter(): TagInputAdapter {
  196. return {
  197. ...super.adapter,
  198. setInputValue: (inputValue: string) => {
  199. this.setState({ inputValue });
  200. },
  201. setTagsArray: (tagsArray: string[]) => {
  202. this.setState({ tagsArray });
  203. },
  204. setFocusing: (focusing: boolean) => {
  205. this.setState({ focusing });
  206. },
  207. toggleFocusing: (isFocus: boolean) => {
  208. const { preventScroll } = this.props;
  209. const input = this.inputRef && this.inputRef.current;
  210. if (isFocus) {
  211. input && input.focus({ preventScroll });
  212. } else {
  213. input && input.blur();
  214. }
  215. this.setState({ focusing: isFocus });
  216. },
  217. setHovering: (hovering: boolean) => {
  218. this.setState({ hovering });
  219. },
  220. setActive: (active: boolean) => {
  221. this.setState({ active });
  222. },
  223. setEntering: (entering: boolean) => {
  224. this.setState({ entering });
  225. },
  226. getClickOutsideHandler: () => {
  227. return this.clickOutsideHandler;
  228. },
  229. notifyBlur: (e: React.MouseEvent<HTMLInputElement>) => {
  230. this.props.onBlur(e);
  231. },
  232. notifyFocus: (e: React.MouseEvent<HTMLInputElement>) => {
  233. this.props.onFocus(e);
  234. },
  235. notifyInputChange: (v: string, e: React.MouseEvent<HTMLInputElement>) => {
  236. this.props.onInputChange(v, e);
  237. },
  238. notifyTagChange: (v: string[]) => {
  239. this.props.onChange(v);
  240. },
  241. notifyTagAdd: (v: string[]) => {
  242. this.props.onAdd(v);
  243. },
  244. notifyTagRemove: (v: string, idx: number) => {
  245. this.props.onRemove(v, idx);
  246. },
  247. notifyKeyDown: e => {
  248. this.props.onKeyDown(e);
  249. },
  250. registerClickOutsideHandler: cb => {
  251. const clickOutsideHandler = (e: Event) => {
  252. const tagInputDom = this.tagInputRef && this.tagInputRef.current;
  253. const target = e.target as Element;
  254. if (tagInputDom && !tagInputDom.contains(target)) {
  255. cb(e);
  256. }
  257. };
  258. this.clickOutsideHandler = clickOutsideHandler;
  259. document.addEventListener('click', clickOutsideHandler, false);
  260. },
  261. unregisterClickOutsideHandler: () => {
  262. document.removeEventListener('click', this.clickOutsideHandler, false);
  263. this.clickOutsideHandler = null;
  264. },
  265. };
  266. }
  267. componentDidMount() {
  268. const { disabled, autoFocus, preventScroll } = this.props;
  269. if (!disabled && autoFocus) {
  270. this.inputRef.current.focus({ preventScroll });
  271. this.foundation.handleClick();
  272. }
  273. this.foundation.init();
  274. }
  275. handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  276. this.foundation.handleInputChange(e);
  277. };
  278. handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
  279. this.foundation.handleKeyDown(e);
  280. };
  281. handleInputFocus = (e: React.MouseEvent<HTMLInputElement>) => {
  282. this.foundation.handleInputFocus(e);
  283. };
  284. handleInputBlur = (e: React.MouseEvent<HTMLInputElement>) => {
  285. this.foundation.handleInputBlur(e);
  286. };
  287. handleClearBtn = (e: React.MouseEvent<HTMLDivElement>) => {
  288. this.foundation.handleClearBtn(e);
  289. };
  290. /* istanbul ignore next */
  291. handleClearEnterPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
  292. this.foundation.handleClearEnterPress(e);
  293. };
  294. handleTagClose = (idx: number) => {
  295. this.foundation.handleTagClose(idx);
  296. };
  297. handleInputMouseLeave = (e: React.MouseEvent<HTMLDivElement>) => {
  298. this.foundation.handleInputMouseLeave();
  299. };
  300. handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
  301. this.foundation.handleClick(e);
  302. };
  303. handleInputMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
  304. this.foundation.handleInputMouseEnter();
  305. };
  306. handleClickPrefixOrSuffix = (e: React.MouseEvent<HTMLInputElement>) => {
  307. this.foundation.handleClickPrefixOrSuffix(e);
  308. };
  309. handlePreventMouseDown = (e: React.MouseEvent<HTMLInputElement>) => {
  310. this.foundation.handlePreventMouseDown(e);
  311. };
  312. renderClearBtn() {
  313. const { hovering, tagsArray, inputValue } = this.state;
  314. const { showClear, disabled, clearIcon } = this.props;
  315. const clearCls = cls(`${prefixCls}-clearBtn`, {
  316. [`${prefixCls}-clearBtn-invisible`]: !hovering || (inputValue === '' && tagsArray.length === 0) || disabled,
  317. });
  318. if (showClear) {
  319. return (
  320. <div
  321. role="button"
  322. tabIndex={0}
  323. aria-label="Clear TagInput value"
  324. className={clearCls}
  325. onClick={e => this.handleClearBtn(e)}
  326. onKeyPress={e => this.handleClearEnterPress(e)}
  327. >
  328. { clearIcon ? clearIcon : <IconClear />}
  329. </div>
  330. );
  331. }
  332. return null;
  333. }
  334. renderPrefix() {
  335. const { prefix, insetLabel, insetLabelId } = this.props;
  336. const labelNode = prefix || insetLabel;
  337. if (isNull(labelNode) || isUndefined(labelNode)) {
  338. return null;
  339. }
  340. const prefixWrapperCls = cls(`${prefixCls}-prefix`, {
  341. [`${prefixCls}-inset-label`]: insetLabel,
  342. [`${prefixCls}-prefix-text`]: labelNode && isString(labelNode),
  343. // eslint-disable-next-line max-len
  344. [`${prefixCls}-prefix-icon`]: isSemiIcon(labelNode),
  345. });
  346. return (
  347. // eslint-disable-next-line jsx-a11y/no-static-element-interactions,jsx-a11y/click-events-have-key-events
  348. <div
  349. className={prefixWrapperCls}
  350. onMouseDown={this.handlePreventMouseDown}
  351. onClick={this.handleClickPrefixOrSuffix}
  352. id={insetLabelId} x-semi-prop="prefix"
  353. >
  354. {labelNode}
  355. </div>
  356. );
  357. }
  358. renderSuffix() {
  359. const { suffix } = this.props;
  360. if (isNull(suffix) || isUndefined(suffix)) {
  361. return null;
  362. }
  363. const suffixWrapperCls = cls(`${prefixCls}-suffix`, {
  364. [`${prefixCls}-suffix-text`]: suffix && isString(suffix),
  365. // eslint-disable-next-line max-len
  366. [`${prefixCls}-suffix-icon`]: isSemiIcon(suffix),
  367. });
  368. return (
  369. // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
  370. <div
  371. className={suffixWrapperCls}
  372. onMouseDown={this.handlePreventMouseDown}
  373. onClick={this.handleClickPrefixOrSuffix}
  374. x-semi-prop="suffix"
  375. >
  376. {suffix}
  377. </div>
  378. );
  379. }
  380. getAllTags = () => {
  381. const {
  382. size,
  383. disabled,
  384. renderTagItem,
  385. showContentTooltip,
  386. draggable,
  387. } = this.props;
  388. const { tagsArray, active } = this.state;
  389. const showIconHandler = active && draggable;
  390. const tagCls = cls(`${prefixCls}-wrapper-tag`, {
  391. [`${prefixCls}-wrapper-tag-size-${size}`]: size,
  392. [`${prefixCls}-wrapper-tag-icon`]: showIconHandler,
  393. });
  394. const typoCls = cls(`${prefixCls}-wrapper-typo`, {
  395. [`${prefixCls}-wrapper-typo-disabled`]: disabled,
  396. });
  397. const itemWrapperCls = cls({
  398. [`${prefixCls}-drag-item`]: showIconHandler,
  399. [`${prefixCls}-wrapper-tag-icon`]: showIconHandler,
  400. });
  401. const DragHandle = SortableHandle(() => <IconHandle className={`${prefixCls}-drag-handler`}></IconHandle>);
  402. return tagsArray.map((value, index) => {
  403. const elementKey = showIconHandler ? value : `${index}${value}`;
  404. const onClose = () => {
  405. !disabled && this.handleTagClose(index);
  406. };
  407. if (isFunction(renderTagItem)) {
  408. return showIconHandler? (<div className={itemWrapperCls} key={elementKey}>
  409. <DragHandle />
  410. {renderTagItem(value, index, onClose)}
  411. </div>) : renderTagItem(value, index, onClose);
  412. } else {
  413. return (
  414. <Tag
  415. className={tagCls}
  416. color="white"
  417. size={size === 'small' ? 'small' : 'large'}
  418. type="light"
  419. onClose={onClose}
  420. closable={!disabled}
  421. key={elementKey}
  422. visible
  423. aria-label={`${!disabled ? 'Closable ' : ''}Tag: ${value}`}
  424. >
  425. {/* Wrap a layer of div outside IconHandler and Value to ensure that the two are aligned */}
  426. <div className={`${prefixCls}-tag-content-wrapper`}>
  427. {showIconHandler && <DragHandle />}
  428. <Paragraph
  429. className={typoCls}
  430. ellipsis={{ showTooltip: showContentTooltip, rows: 1 }}
  431. >
  432. {value}
  433. </Paragraph>
  434. </div>
  435. </Tag>
  436. );
  437. }
  438. });
  439. }
  440. onSortEnd = (callbackProps: OnSortEndProps) => {
  441. this.foundation.handleSortEnd(callbackProps);
  442. }
  443. renderTags() {
  444. const {
  445. disabled,
  446. maxTagCount,
  447. showRestTagsPopover,
  448. restTagsPopoverProps = {},
  449. draggable,
  450. expandRestTagsOnClick,
  451. } = this.props;
  452. const { tagsArray, active } = this.state;
  453. const restTagsCls = cls(`${prefixCls}-wrapper-n`, {
  454. [`${prefixCls}-wrapper-n-disabled`]: disabled,
  455. });
  456. const allTags = this.getAllTags();
  457. let restTags: Array<React.ReactNode> = [];
  458. let tags: Array<React.ReactNode> = [...allTags];
  459. if (( !active || !expandRestTagsOnClick) && maxTagCount && maxTagCount < allTags.length){
  460. tags = allTags.slice(0, maxTagCount);
  461. restTags = allTags.slice(maxTagCount);
  462. }
  463. const restTagsContent = (
  464. <span className={restTagsCls}>+{tagsArray.length - maxTagCount}</span>
  465. );
  466. const sortableListItems = allTags.map((item, index) => ({
  467. item: item,
  468. key: tagsArray[index],
  469. }));
  470. if (active && draggable && sortableListItems.length > 0) {
  471. // helperClass:add styles to the helper(item being dragged) https://github.com/clauderic/react-sortable-hoc/issues/87
  472. // @ts-ignore skip SortableItem type check
  473. return <SortableList useDragHandle helperClass={`${prefixCls}-drag-item-move`} items={sortableListItems} onSortEnd={this.onSortEnd} axis={"xy"} />;
  474. }
  475. return (
  476. <>
  477. {tags}
  478. {
  479. restTags.length > 0 &&
  480. (
  481. showRestTagsPopover ?
  482. (
  483. <Popover
  484. content={restTags}
  485. showArrow
  486. trigger="hover"
  487. position="top"
  488. autoAdjustOverflow
  489. {...restTagsPopoverProps}
  490. >
  491. {restTagsContent}
  492. </Popover>
  493. ) : restTagsContent
  494. )
  495. }
  496. </>
  497. );
  498. }
  499. blur() {
  500. this.inputRef.current.blur();
  501. // unregister clickOutside event
  502. this.foundation.clickOutsideCallBack();
  503. }
  504. focus() {
  505. const { preventScroll, disabled } = this.props;
  506. this.inputRef.current.focus({ preventScroll });
  507. if (!disabled) {
  508. // register clickOutside event
  509. this.foundation.handleClick();
  510. }
  511. }
  512. handleInputCompositionStart = (e) => {
  513. this.foundation.handleInputCompositionStart(e);
  514. }
  515. handleInputCompositionEnd = (e) => {
  516. this.foundation.handleInputCompositionEnd(e);
  517. }
  518. render() {
  519. const {
  520. size,
  521. style,
  522. className,
  523. disabled,
  524. placeholder,
  525. validateStatus,
  526. } = this.props;
  527. const {
  528. focusing,
  529. hovering,
  530. tagsArray,
  531. inputValue,
  532. active,
  533. } = this.state;
  534. const tagInputCls = cls(prefixCls, className, {
  535. [`${prefixCls}-focus`]: focusing || active,
  536. [`${prefixCls}-disabled`]: disabled,
  537. [`${prefixCls}-hover`]: hovering && !disabled,
  538. [`${prefixCls}-error`]: validateStatus === 'error',
  539. [`${prefixCls}-warning`]: validateStatus === 'warning',
  540. [`${prefixCls}-small`]: size === 'small',
  541. [`${prefixCls}-large`]: size === 'large',
  542. });
  543. const inputCls = cls(`${prefixCls}-wrapper-input`, `${prefixCls}-wrapper-input-${size}`);
  544. const wrapperCls = cls(`${prefixCls}-wrapper`);
  545. return (
  546. // eslint-disable-next-line
  547. <div
  548. ref={this.tagInputRef}
  549. style={style}
  550. className={tagInputCls}
  551. aria-disabled={disabled}
  552. aria-label={this.props['aria-label']}
  553. aria-invalid={validateStatus === 'error'}
  554. onMouseEnter={e => {
  555. this.handleInputMouseEnter(e);
  556. }}
  557. onMouseLeave={e => {
  558. this.handleInputMouseLeave(e);
  559. }}
  560. onClick={e => {
  561. this.handleClick(e);
  562. }}
  563. >
  564. {this.renderPrefix()}
  565. <div className={wrapperCls}>
  566. {this.renderTags()}
  567. <Input
  568. aria-label='input value'
  569. ref={this.inputRef as any}
  570. className={inputCls}
  571. disabled={disabled}
  572. value={inputValue}
  573. size={size}
  574. placeholder={tagsArray.length === 0 ? placeholder : ''}
  575. onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
  576. this.handleKeyDown(e);
  577. }}
  578. onChange={(v: string, e: React.ChangeEvent<HTMLInputElement>) => {
  579. this.handleInputChange(e);
  580. }}
  581. onBlur={(e: React.FocusEvent<HTMLInputElement>) => {
  582. this.handleInputBlur(e as any);
  583. }}
  584. onFocus={(e: React.FocusEvent<HTMLInputElement>) => {
  585. this.handleInputFocus(e as any);
  586. }}
  587. onCompositionStart={this.handleInputCompositionStart}
  588. onCompositionEnd={this.handleInputCompositionEnd}
  589. />
  590. </div>
  591. {this.renderClearBtn()}
  592. {this.renderSuffix()}
  593. </div>
  594. );
  595. }
  596. }
  597. export default TagInput;
  598. export { ValidateStatus };