@douyinfe/semi-ui 2.16.1 → 2.17.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/collapse/__test__/collapse.test.js +22 -2
  2. package/collapse/_story/accordion.stories.js +2 -2
  3. package/collapse/item.tsx +20 -6
  4. package/datePicker/__test__/datePicker.test.js +5 -5
  5. package/datePicker/_story/datePicker.stories.js +138 -22
  6. package/datePicker/datePicker.tsx +42 -7
  7. package/datePicker/monthsGrid.tsx +22 -10
  8. package/datePicker/quickControl.tsx +62 -16
  9. package/datePicker/yearAndMonth.tsx +31 -5
  10. package/dist/css/semi.css +289 -32
  11. package/dist/css/semi.min.css +1 -1
  12. package/dist/umd/semi-ui.js +45706 -45192
  13. package/dist/umd/semi-ui.js.map +1 -1
  14. package/dist/umd/semi-ui.min.js +1 -1
  15. package/dist/umd/semi-ui.min.js.map +1 -1
  16. package/lib/cjs/collapse/item.d.ts +8 -0
  17. package/lib/cjs/collapse/item.js +19 -8
  18. package/lib/cjs/datePicker/datePicker.d.ts +3 -0
  19. package/lib/cjs/datePicker/datePicker.js +56 -9
  20. package/lib/cjs/datePicker/monthsGrid.d.ts +3 -0
  21. package/lib/cjs/datePicker/monthsGrid.js +14 -3
  22. package/lib/cjs/datePicker/quickControl.d.ts +6 -0
  23. package/lib/cjs/datePicker/quickControl.js +61 -14
  24. package/lib/cjs/datePicker/yearAndMonth.d.ts +3 -0
  25. package/lib/cjs/datePicker/yearAndMonth.js +15 -3
  26. package/lib/cjs/popover/index.d.ts +3 -0
  27. package/lib/cjs/popover/index.js +4 -2
  28. package/lib/cjs/select/index.d.ts +6 -1
  29. package/lib/cjs/select/index.js +130 -72
  30. package/lib/cjs/select/option.js +4 -2
  31. package/lib/cjs/tag/index.js +6 -4
  32. package/lib/cjs/tag/interface.d.ts +1 -0
  33. package/lib/cjs/tagInput/index.d.ts +13 -1
  34. package/lib/cjs/tagInput/index.js +217 -91
  35. package/lib/cjs/tooltip/index.d.ts +4 -0
  36. package/lib/cjs/tooltip/index.js +5 -3
  37. package/lib/es/collapse/item.d.ts +8 -0
  38. package/lib/es/collapse/item.js +19 -8
  39. package/lib/es/datePicker/datePicker.d.ts +3 -0
  40. package/lib/es/datePicker/datePicker.js +56 -9
  41. package/lib/es/datePicker/monthsGrid.d.ts +3 -0
  42. package/lib/es/datePicker/monthsGrid.js +14 -3
  43. package/lib/es/datePicker/quickControl.d.ts +6 -0
  44. package/lib/es/datePicker/quickControl.js +61 -15
  45. package/lib/es/datePicker/yearAndMonth.d.ts +3 -0
  46. package/lib/es/datePicker/yearAndMonth.js +14 -3
  47. package/lib/es/popover/index.d.ts +3 -0
  48. package/lib/es/popover/index.js +4 -2
  49. package/lib/es/select/index.d.ts +6 -1
  50. package/lib/es/select/index.js +129 -71
  51. package/lib/es/select/option.js +4 -2
  52. package/lib/es/tag/index.js +6 -4
  53. package/lib/es/tag/interface.d.ts +1 -0
  54. package/lib/es/tagInput/index.d.ts +13 -1
  55. package/lib/es/tagInput/index.js +217 -93
  56. package/lib/es/tooltip/index.d.ts +4 -0
  57. package/lib/es/tooltip/index.js +5 -3
  58. package/package.json +7 -7
  59. package/popover/index.tsx +4 -1
  60. package/select/__test__/select.test.js +5 -3
  61. package/select/index.tsx +65 -30
  62. package/select/option.tsx +2 -0
  63. package/tag/index.tsx +3 -2
  64. package/tag/interface.ts +1 -0
  65. package/tagInput/_story/tagInput.stories.js +18 -0
  66. package/tagInput/index.tsx +126 -26
  67. package/tooltip/index.tsx +5 -2
package/select/index.tsx CHANGED
@@ -24,7 +24,7 @@ import OptionGroup from './optionGroup';
24
24
  import Spin from '../spin';
25
25
  import Trigger from '../trigger';
26
26
  import { IconChevronDown, IconClear } from '@douyinfe/semi-icons';
27
- import { isSemiIcon } from '../_utils';
27
+ import { isSemiIcon, getFocusableElements, getActiveElement } from '../_utils';
28
28
  import { Subtract } from 'utility-types';
29
29
 
30
30
  import warning from '@douyinfe/semi-foundation/utils/warning';
@@ -177,6 +177,7 @@ export interface SelectState {
177
177
  keyboardEventSet: any; // {}
178
178
  optionGroups: Array<any>;
179
179
  isHovering: boolean;
180
+ isFocusInContainer: boolean;
180
181
  }
181
182
 
182
183
  // 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
@@ -304,13 +305,13 @@ class Select extends BaseComponent<SelectProps, SelectState> {
304
305
  onListScroll: noop,
305
306
  maxHeight: 300,
306
307
  dropdownMatchSelectWidth: true,
307
- defaultActiveFirstOption: false,
308
+ defaultActiveFirstOption: true, // In order to meet the needs of A11y, change to true
308
309
  showArrow: true,
309
310
  showClear: false,
310
311
  remote: false,
311
312
  autoAdjustOverflow: true,
312
313
  autoClearSearchValue: true,
313
- arrowIcon: <IconChevronDown />
314
+ arrowIcon: <IconChevronDown aria-label='' />
314
315
  // Radio selection is different from the default renderSelectedItem for multiple selection, so it is not declared here
315
316
  // renderSelectedItem: (optionNode) => optionNode.label,
316
317
  // The default creator rendering is related to i18, so it is not declared here
@@ -319,9 +320,11 @@ class Select extends BaseComponent<SelectProps, SelectState> {
319
320
 
320
321
  inputRef: React.RefObject<HTMLInputElement>;
321
322
  triggerRef: React.RefObject<HTMLDivElement>;
323
+ optionContainerEl: React.RefObject<HTMLDivElement>;
322
324
  optionsRef: React.RefObject<any>;
323
325
  virtualizeListRef: React.RefObject<any>;
324
326
  selectOptionListID: string;
327
+ selectID: string;
325
328
  clickOutsideHandler: (e: MouseEvent) => void;
326
329
  foundation: SelectFoundation;
327
330
  context: ContextValue;
@@ -341,13 +344,16 @@ class Select extends BaseComponent<SelectProps, SelectState> {
341
344
  keyboardEventSet: {},
342
345
  optionGroups: [],
343
346
  isHovering: false,
347
+ isFocusInContainer: false,
344
348
  };
345
349
  /* Generate random string */
346
350
  this.selectOptionListID = '';
351
+ this.selectID = '';
347
352
  this.virtualizeListRef = React.createRef();
348
353
  this.inputRef = React.createRef();
349
354
  this.triggerRef = React.createRef();
350
355
  this.optionsRef = React.createRef();
356
+ this.optionContainerEl = React.createRef();
351
357
  this.clickOutsideHandler = null;
352
358
  this.onSelect = this.onSelect.bind(this);
353
359
  this.onClear = this.onClear.bind(this);
@@ -355,7 +361,6 @@ class Select extends BaseComponent<SelectProps, SelectState> {
355
361
  this.onMouseLeave = this.onMouseLeave.bind(this);
356
362
  this.renderOption = this.renderOption.bind(this);
357
363
  this.onKeyPress = this.onKeyPress.bind(this);
358
- this.onClearBtnEnterPress = this.onClearBtnEnterPress.bind(this);
359
364
 
360
365
  this.foundation = new SelectFoundation(this.adapter);
361
366
 
@@ -370,6 +375,8 @@ class Select extends BaseComponent<SelectProps, SelectState> {
370
375
  );
371
376
  }
372
377
 
378
+ setOptionContainerEl = (node: HTMLDivElement) => (this.optionContainerEl = { current: node });
379
+
373
380
  get adapter(): SelectAdapter<SelectProps, SelectState> {
374
381
  const keyboardAdapter = {
375
382
  registerKeyDown: (cb: () => void) => {
@@ -536,6 +543,21 @@ class Select extends BaseComponent<SelectProps, SelectState> {
536
543
 
537
544
  }
538
545
  },
546
+ getContainer: () => {
547
+ return this.optionContainerEl && this.optionContainerEl.current;
548
+ },
549
+ getFocusableElements: (node: HTMLDivElement) => {
550
+ return getFocusableElements(node);
551
+ },
552
+ getActiveElement: () => {
553
+ return getActiveElement();
554
+ },
555
+ setIsFocusInContainer: (isFocusInContainer: boolean) => {
556
+ this.setState({ isFocusInContainer });
557
+ },
558
+ getIsFocusInContainer: () => {
559
+ return this.state.isFocusInContainer;
560
+ },
539
561
  updateScrollTop: (index?: number) => {
540
562
  // eslint-disable-next-line max-len
541
563
  let optionClassName = `.${prefixcls}-option-selected`;
@@ -565,6 +587,7 @@ class Select extends BaseComponent<SelectProps, SelectState> {
565
587
  componentDidMount() {
566
588
  this.foundation.init();
567
589
  this.selectOptionListID = getUuidShort();
590
+ this.selectID = this.props.id || getUuidShort();
568
591
  }
569
592
 
570
593
  componentWillUnmount() {
@@ -595,13 +618,13 @@ class Select extends BaseComponent<SelectProps, SelectState> {
595
618
  handleInputChange = (value: string) => this.foundation.handleInputChange(value);
596
619
 
597
620
  renderInput() {
598
- const { size, multiple, disabled, inputProps } = this.props;
621
+ const { size, multiple, disabled, inputProps, filter } = this.props;
599
622
  const inputPropsCls = get(inputProps, 'className');
600
623
  const inputcls = cls(`${prefixcls}-input`, {
601
624
  [`${prefixcls}-input-single`]: !multiple,
602
625
  [`${prefixcls}-input-multiple`]: multiple,
603
626
  }, inputPropsCls);
604
- const { inputValue } = this.state;
627
+ const { inputValue, focusIndex } = this.state;
605
628
 
606
629
  const selectInputProps: Record<string, any> = {
607
630
  value: inputValue,
@@ -623,11 +646,18 @@ class Select extends BaseComponent<SelectProps, SelectState> {
623
646
  <Input
624
647
  ref={this.inputRef as any}
625
648
  size={size}
649
+ aria-activedescendant={focusIndex !== -1 ? `${this.selectID}-option-${focusIndex}`: ''}
626
650
  onFocus={(e: React.FocusEvent<HTMLInputElement>) => {
651
+ // if multiple and filter, when use tab key to let select get focus
652
+ // need to manual update state isFocus to let the focus style take effect
653
+ if (multiple && Boolean(filter)){
654
+ this.setState({ isFocus: true });
655
+ }
627
656
  // prevent event bubbling which will fire trigger onFocus event
628
657
  e.stopPropagation();
629
658
  // e.nativeEvent.stopImmediatePropagation();
630
659
  }}
660
+ onBlur={e => this.foundation.handleInputBlur(e)}
631
661
  {...selectInputProps}
632
662
  />
633
663
  );
@@ -666,10 +696,6 @@ class Select extends BaseComponent<SelectProps, SelectState> {
666
696
  this.foundation.handleClearClick(e as any);
667
697
  }
668
698
 
669
- /* istanbul ignore next */
670
- onClearBtnEnterPress(e: React.KeyboardEvent) {
671
- this.foundation.handleClearBtnEnterPress(e as any);
672
- }
673
699
 
674
700
  renderEmpty() {
675
701
  return <Option empty={true} emptyContent={this.props.emptyContent} />;
@@ -712,6 +738,7 @@ class Select extends BaseComponent<SelectProps, SelectState> {
712
738
  key={option.key || option.label as string + option.value as string + optionIndex}
713
739
  renderOptionItem={renderOptionItem}
714
740
  inputValue={inputValue}
741
+ id={`${this.selectID}-option-${optionIndex}`}
715
742
  >
716
743
  {option.label}
717
744
  </Option>
@@ -837,7 +864,14 @@ class Select extends BaseComponent<SelectProps, SelectState> {
837
864
 
838
865
  const isEmpty = !options.length || !options.some(item => item._show);
839
866
  return (
840
- <div id={`${prefixcls}-${this.selectOptionListID}`} className={dropdownClassName} style={style}>
867
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions
868
+ <div
869
+ id={`${prefixcls}-${this.selectOptionListID}`}
870
+ className={dropdownClassName}
871
+ style={style}
872
+ ref={this.setOptionContainerEl}
873
+ onKeyDown={e => this.foundation.handleContainerKeyDown(e)}
874
+ >
841
875
  {outerTopSlot}
842
876
  <div
843
877
  style={{ maxHeight: `${maxHeight}px` }}
@@ -930,7 +964,7 @@ class Select extends BaseComponent<SelectProps, SelectState> {
930
964
  };
931
965
  if (isRenderInTag) {
932
966
  return (
933
- <Tag {...basic} color="white" size={size || 'large'} key={value}>
967
+ <Tag {...basic} color="white" size={size || 'large'} key={value} tabIndex={-1}>
934
968
  {content}
935
969
  </Tag>
936
970
  );
@@ -956,7 +990,7 @@ class Select extends BaseComponent<SelectProps, SelectState> {
956
990
 
957
991
  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
958
992
 
959
- const tagContent = NotOneLine ? tags : <TagGroup<"custom"> tagList={tags} maxTagCount={n} restCount={maxTagCount ? selectedItems.length - maxTagCount : undefined} size="large" mode="custom" />;
993
+ const tagContent = NotOneLine ? tags : <TagGroup<"custom"> tagList={tags} maxTagCount={n} restCount={maxTagCount ? selectedItems.length - maxTagCount : undefined} size="large" mode="custom"/>;
960
994
 
961
995
  return (
962
996
  <>
@@ -1055,7 +1089,7 @@ class Select extends BaseComponent<SelectProps, SelectState> {
1055
1089
  arrowIcon,
1056
1090
  } = this.props;
1057
1091
 
1058
- const { selections, isOpen, keyboardEventSet, inputValue, isHovering, isFocus } = this.state;
1092
+ const { selections, isOpen, keyboardEventSet, inputValue, isHovering, isFocus, showInput, focusIndex } = this.state;
1059
1093
  const useCustomTrigger = typeof triggerRender === 'function';
1060
1094
  const filterable = Boolean(filter); // filter(boolean || function)
1061
1095
  const selectionCls = useCustomTrigger ?
@@ -1109,32 +1143,31 @@ class Select extends BaseComponent<SelectProps, SelectState> {
1109
1143
  </div>
1110
1144
  </Fragment>,
1111
1145
  <Fragment key="clearicon">
1112
- {showClear ? (
1113
- <div
1114
- role="button"
1115
- aria-label="Clear selected value"
1116
- tabIndex={0}
1117
- className={cls(`${prefixcls}-clear`)}
1118
- onClick={this.onClear}
1119
- onKeyPress={this.onClearBtnEnterPress}
1120
- >
1121
- <IconClear />
1122
- </div>
1123
- ) : arrowContent}
1146
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
1147
+ {showClear ? ( <div className={cls(`${prefixcls}-clear`)} onClick={this.onClear}><IconClear /></div>) : arrowContent}
1124
1148
  </Fragment>,
1125
1149
  <Fragment key="suffix">{suffix ? this.renderSuffix() : null}</Fragment>,
1126
1150
  ]
1127
1151
  );
1128
1152
 
1129
- const tabIndex = disabled ? null : 0;
1153
+ /**
1154
+ *
1155
+ * In disabled, searchable single-selection and display input, and searchable multi-selection
1156
+ * make combobox not focusable by tab key
1157
+ *
1158
+ * 在disabled,可搜索单选且显示input框,以及可搜索多选情况下
1159
+ * 让combobox无法通过tab聚焦
1160
+ */
1161
+ const tabIndex = (disabled || (filterable && showInput) || (filterable && multiple)) ? -1 : 0;
1130
1162
  return (
1163
+ /* eslint-disable-next-line jsx-a11y/aria-activedescendant-has-tabindex */
1131
1164
  <div
1132
1165
  role="combobox"
1133
1166
  aria-disabled={disabled}
1134
1167
  aria-expanded={isOpen}
1135
1168
  aria-controls={`${prefixcls}-${this.selectOptionListID}`}
1136
1169
  aria-haspopup="listbox"
1137
- aria-label="select value"
1170
+ aria-label={selections.size ? 'selected' : ''} // if there is a value, expect the narration to speak selected
1138
1171
  aria-invalid={this.props['aria-invalid']}
1139
1172
  aria-errormessage={this.props['aria-errormessage']}
1140
1173
  aria-labelledby={this.props['aria-labelledby']}
@@ -1144,11 +1177,12 @@ class Select extends BaseComponent<SelectProps, SelectState> {
1144
1177
  ref={ref => ((this.triggerRef as any).current = ref)}
1145
1178
  onClick={e => this.foundation.handleClick(e)}
1146
1179
  style={style}
1147
- id={id}
1180
+ id={this.selectID}
1148
1181
  tabIndex={tabIndex}
1182
+ aria-activedescendant={focusIndex !== -1 ? `${this.selectID}-option-${focusIndex}`: ''}
1149
1183
  onMouseEnter={this.onMouseEnter}
1150
1184
  onMouseLeave={this.onMouseLeave}
1151
- // onFocus={e => this.foundation.handleTriggerFocus(e)}
1185
+ onFocus={e => this.foundation.handleTriggerFocus(e)}
1152
1186
  onBlur={e => this.foundation.handleTriggerBlur(e as any)}
1153
1187
  onKeyPress={this.onKeyPress}
1154
1188
  {...keyboardEventSet}
@@ -1193,6 +1227,7 @@ class Select extends BaseComponent<SelectProps, SelectState> {
1193
1227
  position={position}
1194
1228
  spacing={spacing}
1195
1229
  stopPropagation={stopPropagation}
1230
+ disableArrowKeyDown={true}
1196
1231
  onVisibleChange={status => this.handlePopoverVisibleChange(status)}
1197
1232
  >
1198
1233
  {selection}
package/select/option.tsx CHANGED
@@ -88,6 +88,7 @@ class Option extends PureComponent<OptionProps> {
88
88
  prefixCls,
89
89
  renderOptionItem,
90
90
  inputValue,
91
+ id,
91
92
  ...rest
92
93
  } = this.props;
93
94
  const optionClassName = classNames(prefixCls, {
@@ -146,6 +147,7 @@ class Option extends PureComponent<OptionProps> {
146
147
  }}
147
148
  onMouseEnter={e => onMouseEnter && onMouseEnter(e)}
148
149
  role="option"
150
+ id={id}
149
151
  aria-selected={selected ? "true" : "false"}
150
152
  aria-disabled={disabled ? "true" : "false"}
151
153
  style={style}
package/tag/index.tsx CHANGED
@@ -119,10 +119,11 @@ export default class Tag extends Component<TagProps, TagState> {
119
119
  }
120
120
 
121
121
  render() {
122
- const { children, size, color, closable, visible, onClose, onClick, className, type, avatarSrc, avatarShape, ...attr } = this.props;
122
+ const { children, size, color, closable, visible, onClose, onClick, className, type, avatarSrc, avatarShape, tabIndex, ...attr } = this.props;
123
123
  const { visible: isVisible } = this.state;
124
124
  const clickable = onClick !== Tag.defaultProps.onClick || closable;
125
- const a11yProps = { role: 'button', tabIndex: 0, onKeyDown: this.handleKeyDown };
125
+ // only when the Tag is clickable or closable, the value of tabIndex is allowed to be passed in.
126
+ const a11yProps = { role: 'button', tabIndex: tabIndex | 0, onKeyDown: this.handleKeyDown };
126
127
  const baseProps = {
127
128
  ...attr,
128
129
  onClick,
package/tag/interface.ts CHANGED
@@ -35,6 +35,7 @@ export interface TagProps {
35
35
  avatarShape?: AvatarShape;
36
36
  onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
37
37
  'aria-label'?: React.AriaAttributes['aria-label'];
38
+ tabIndex?: number; // use internal, when tag in taInput, we want to use left arrow and right arrow to control the tag focus, so the tabIndex need to be -1.
38
39
  }
39
40
 
40
41
  export interface TagGroupProps {
@@ -78,6 +78,24 @@ ShowClear.story = {
78
78
  name: 'showClear',
79
79
  };
80
80
 
81
+ export const Draggable = () => (
82
+ <>
83
+ <TagInput draggable defaultValue={['抖音', '火山', '西瓜视频', 'AI Lab', '花亦山', '水之月','轻颜','醒图']} showClear style={style} />
84
+ <br />
85
+ <TagInput
86
+ draggable
87
+ defaultValue={['抖音', '火山', '西瓜视频', 'AI Lab', '花亦山', '水之月','轻颜','醒图']}
88
+ maxTagCount={5}
89
+ showClear
90
+ style={style}
91
+ />
92
+ </>
93
+ );
94
+
95
+ Draggable.story = {
96
+ name: 'draggable',
97
+ };
98
+
81
99
  export const MaxExceed = () => (
82
100
  <>
83
101
  <TagInput
@@ -11,7 +11,7 @@ import {
11
11
  } from 'lodash';
12
12
  import { cssClasses, strings } from '@douyinfe/semi-foundation/tagInput/constants';
13
13
  import '@douyinfe/semi-foundation/tagInput/tagInput.scss';
14
- import TagInputFoundation, { TagInputAdapter } from '@douyinfe/semi-foundation/tagInput/foundation';
14
+ import TagInputFoundation, { TagInputAdapter, OnSortEndProps } from '@douyinfe/semi-foundation/tagInput/foundation';
15
15
  import { ArrayElement } from '../_base/base';
16
16
  import { isSemiIcon } from '../_utils';
17
17
  import BaseComponent from '../_base/baseComponent';
@@ -19,12 +19,27 @@ import Tag from '../tag';
19
19
  import Input from '../input';
20
20
  import Popover, { PopoverProps } from '../popover';
21
21
  import Paragraph from '../typography/paragraph';
22
- import { IconClear } from '@douyinfe/semi-icons';
22
+ import { IconClear, IconHandle } from '@douyinfe/semi-icons';
23
+ import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
23
24
 
24
25
  export type Size = ArrayElement<typeof strings.SIZE_SET>;
25
26
  export type RestTagsPopoverProps = PopoverProps;
26
27
  type ValidateStatus = "default" | "error" | "warning";
27
28
 
29
+ const SortableItem = SortableElement(props => props.item);
30
+
31
+ const SortableList = SortableContainer(
32
+ ({ items }) => {
33
+ return (
34
+ <div style={{ display: 'flex', flexFlow: 'row wrap', }}>
35
+ {items.map((item, index) => (
36
+ // @ts-ignore skip SortableItem type check
37
+ <SortableItem key={item.key} index={index} item={item.item}></SortableItem>
38
+ ))}
39
+ </div>
40
+ );
41
+ });
42
+
28
43
  export interface TagInputProps {
29
44
  className?: string;
30
45
  defaultValue?: string[];
@@ -38,6 +53,8 @@ export interface TagInputProps {
38
53
  showContentTooltip?: boolean;
39
54
  allowDuplicates?: boolean;
40
55
  addOnBlur?: boolean;
56
+ draggable?: boolean;
57
+ expandRestTagsOnClick?: boolean;
41
58
  onAdd?: (addedValue: string[]) => void;
42
59
  onBlur?: (e: React.MouseEvent<HTMLInputElement>) => void;
43
60
  onChange?: (value: string[]) => void;
@@ -69,6 +86,7 @@ export interface TagInputState {
69
86
  inputValue?: string;
70
87
  focusing?: boolean;
71
88
  hovering?: boolean;
89
+ active?: boolean;
72
90
  }
73
91
 
74
92
  const prefixCls = cssClasses.PREFIX;
@@ -93,6 +111,8 @@ class TagInput extends BaseComponent<TagInputProps, TagInputState> {
93
111
  separator: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
94
112
  showClear: PropTypes.bool,
95
113
  addOnBlur: PropTypes.bool,
114
+ draggable: PropTypes.bool,
115
+ expandRestTagsOnClick: PropTypes.bool,
96
116
  autoFocus: PropTypes.bool,
97
117
  renderTagItem: PropTypes.func,
98
118
  onBlur: PropTypes.func,
@@ -118,6 +138,8 @@ class TagInput extends BaseComponent<TagInputProps, TagInputState> {
118
138
  allowDuplicates: true,
119
139
  showRestTagsPopover: true,
120
140
  autoFocus: false,
141
+ draggable: false,
142
+ expandRestTagsOnClick: true,
121
143
  showContentTooltip: true,
122
144
  separator: ',',
123
145
  size: 'default' as const,
@@ -134,7 +156,9 @@ class TagInput extends BaseComponent<TagInputProps, TagInputState> {
134
156
  };
135
157
 
136
158
  inputRef: React.RefObject<HTMLInputElement>;
159
+ tagInputRef: React.RefObject<HTMLDivElement>;
137
160
  foundation: TagInputFoundation;
161
+ clickOutsideHandler: any;
138
162
 
139
163
  constructor(props: TagInputProps) {
140
164
  super(props);
@@ -143,9 +167,12 @@ class TagInput extends BaseComponent<TagInputProps, TagInputState> {
143
167
  tagsArray: props.defaultValue || [],
144
168
  inputValue: '',
145
169
  focusing: false,
146
- hovering: false
170
+ hovering: false,
171
+ active: false,
147
172
  };
148
173
  this.inputRef = React.createRef();
174
+ this.tagInputRef = React.createRef();
175
+ this.clickOutsideHandler = null;
149
176
  }
150
177
 
151
178
  static getDerivedStateFromProps(nextProps: TagInputProps, prevState: TagInputState) {
@@ -190,6 +217,12 @@ class TagInput extends BaseComponent<TagInputProps, TagInputState> {
190
217
  setHovering: (hovering: boolean) => {
191
218
  this.setState({ hovering });
192
219
  },
220
+ setActive: (active: boolean) => {
221
+ this.setState({ active });
222
+ },
223
+ getClickOutsideHandler: () => {
224
+ return this.clickOutsideHandler;
225
+ },
193
226
  notifyBlur: (e: React.MouseEvent<HTMLInputElement>) => {
194
227
  this.props.onBlur(e);
195
228
  },
@@ -211,6 +244,21 @@ class TagInput extends BaseComponent<TagInputProps, TagInputState> {
211
244
  notifyKeyDown: e => {
212
245
  this.props.onKeyDown(e);
213
246
  },
247
+ registerClickOutsideHandler: cb => {
248
+ const clickOutsideHandler = (e: Event) => {
249
+ const tagInputDom = this.tagInputRef && this.tagInputRef.current;
250
+ const target = e.target as Element;
251
+ if (tagInputDom && !tagInputDom.contains(target)) {
252
+ cb(e);
253
+ }
254
+ };
255
+ this.clickOutsideHandler = clickOutsideHandler;
256
+ document.addEventListener('click', clickOutsideHandler, false);
257
+ },
258
+ unregisterClickOutsideHandler: () => {
259
+ document.removeEventListener('click', this.clickOutsideHandler, false);
260
+ this.clickOutsideHandler = null;
261
+ },
214
262
  };
215
263
  }
216
264
 
@@ -218,7 +266,9 @@ class TagInput extends BaseComponent<TagInputProps, TagInputState> {
218
266
  const { disabled, autoFocus, preventScroll } = this.props;
219
267
  if (!disabled && autoFocus) {
220
268
  this.inputRef.current.focus({ preventScroll });
269
+ this.foundation.handleClick();
221
270
  }
271
+ this.foundation.init();
222
272
  }
223
273
 
224
274
  handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -254,6 +304,10 @@ class TagInput extends BaseComponent<TagInputProps, TagInputState> {
254
304
  this.foundation.handleInputMouseLeave();
255
305
  };
256
306
 
307
+ handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
308
+ this.foundation.handleClick(e);
309
+ };
310
+
257
311
  handleInputMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
258
312
  this.foundation.handleInputMouseEnter();
259
313
  };
@@ -338,34 +392,37 @@ class TagInput extends BaseComponent<TagInputProps, TagInputState> {
338
392
  );
339
393
  }
340
394
 
341
- renderTags() {
395
+ getAllTags = () => {
342
396
  const {
343
397
  size,
344
398
  disabled,
345
399
  renderTagItem,
346
- maxTagCount,
347
400
  showContentTooltip,
348
- showRestTagsPopover,
349
- restTagsPopoverProps = {},
401
+ draggable,
350
402
  } = this.props;
351
- const { tagsArray } = this.state;
403
+ const { tagsArray, active } = this.state;
404
+ const showIconHandler = active && draggable;
352
405
  const tagCls = cls(`${prefixCls}-wrapper-tag`, {
353
406
  [`${prefixCls}-wrapper-tag-size-${size}`]: size,
407
+ [`${prefixCls}-wrapper-tag-icon`]: showIconHandler,
354
408
  });
355
409
  const typoCls = cls(`${prefixCls}-wrapper-typo`, {
356
- [`${prefixCls}-wrapper-typo-disabled`]: disabled
410
+ [`${prefixCls}-wrapper-typo-disabled`]: disabled,
357
411
  });
358
- const restTagsCls = cls(`${prefixCls}-wrapper-n`, {
359
- [`${prefixCls}-wrapper-n-disabled`]: disabled
412
+ const itemWrapperCls = cls({
413
+ [`${prefixCls}-drag-item`]: showIconHandler,
414
+ [`${prefixCls}-wrapper-tag-icon`]: showIconHandler,
360
415
  });
361
- const restTags: Array<React.ReactNode> = [];
362
- const tags: Array<React.ReactNode> = [];
363
- tagsArray.forEach((value, index) => {
364
- let item = null;
416
+ const DragHandle = SortableHandle(() => <IconHandle className={`${prefixCls}-drag-handler`}></IconHandle>);
417
+ return tagsArray.map((value, index) => {
418
+ const elementKey = showIconHandler ? value : `${index}${value}`;
365
419
  if (isFunction(renderTagItem)) {
366
- item = renderTagItem(value, index);
420
+ return showIconHandler? (<div className={itemWrapperCls} key={elementKey}>
421
+ <DragHandle />
422
+ {renderTagItem(value, index)}
423
+ </div>) : renderTagItem(value, index);
367
424
  } else {
368
- item = (
425
+ return (
369
426
  <Tag
370
427
  className={tagCls}
371
428
  color="white"
@@ -375,10 +432,11 @@ class TagInput extends BaseComponent<TagInputProps, TagInputState> {
375
432
  !disabled && this.handleTagClose(index);
376
433
  }}
377
434
  closable={!disabled}
378
- key={`${index}${value}`}
435
+ key={elementKey}
379
436
  visible
380
437
  aria-label={`${!disabled ? 'Closable ' : ''}Tag: ${value}`}
381
438
  >
439
+ {showIconHandler && <DragHandle />}
382
440
  <Paragraph
383
441
  className={typoCls}
384
442
  ellipsis={{ showTooltip: showContentTooltip, rows: 1 }}
@@ -388,17 +446,47 @@ class TagInput extends BaseComponent<TagInputProps, TagInputState> {
388
446
  </Tag>
389
447
  );
390
448
  }
391
- if (maxTagCount && index >= maxTagCount) {
392
- restTags.push(item);
393
- } else {
394
- tags.push(item);
395
- }
396
449
  });
450
+ }
397
451
 
452
+ onSortEnd = (callbackProps: OnSortEndProps) => {
453
+ this.foundation.handleSortEnd(callbackProps);
454
+ }
455
+
456
+ renderTags() {
457
+ const {
458
+ disabled,
459
+ maxTagCount,
460
+ showRestTagsPopover,
461
+ restTagsPopoverProps = {},
462
+ draggable,
463
+ expandRestTagsOnClick,
464
+ } = this.props;
465
+ const { tagsArray, active } = this.state;
466
+ const restTagsCls = cls(`${prefixCls}-wrapper-n`, {
467
+ [`${prefixCls}-wrapper-n-disabled`]: disabled,
468
+ });
469
+ const allTags = this.getAllTags();
470
+ let restTags: Array<React.ReactNode> = [];
471
+ let tags: Array<React.ReactNode> = [...allTags];
472
+ if (( !active || !expandRestTagsOnClick) && maxTagCount && maxTagCount < allTags.length){
473
+ tags = allTags.slice(0, maxTagCount);
474
+ restTags = allTags.slice(maxTagCount);
475
+ }
476
+
398
477
  const restTagsContent = (
399
478
  <span className={restTagsCls}>+{tagsArray.length - maxTagCount}</span>
400
479
  );
401
480
 
481
+ const sortableListItems = allTags.map((item, index) => ({
482
+ item: item,
483
+ key: tagsArray[index],
484
+ }));
485
+
486
+ if (active && draggable && sortableListItems.length > 0) {
487
+ // @ts-ignore skip SortableItem type check
488
+ return <SortableList useDragHandle items={sortableListItems} onSortEnd={this.onSortEnd} axis={"xy"} />;
489
+ }
402
490
  return (
403
491
  <>
404
492
  {tags}
@@ -426,11 +514,17 @@ class TagInput extends BaseComponent<TagInputProps, TagInputState> {
426
514
 
427
515
  blur() {
428
516
  this.inputRef.current.blur();
517
+ // unregister clickOutside event
518
+ this.foundation.clickOutsideCallBack();
429
519
  }
430
520
 
431
521
  focus() {
432
- const { preventScroll } = this.props;
522
+ const { preventScroll, disabled } = this.props;
433
523
  this.inputRef.current.focus({ preventScroll });
524
+ if (!disabled) {
525
+ // register clickOutside event
526
+ this.foundation.handleClick();
527
+ }
434
528
  }
435
529
 
436
530
  render() {
@@ -447,11 +541,12 @@ class TagInput extends BaseComponent<TagInputProps, TagInputState> {
447
541
  focusing,
448
542
  hovering,
449
543
  tagsArray,
450
- inputValue
544
+ inputValue,
545
+ active,
451
546
  } = this.state;
452
547
 
453
548
  const tagInputCls = cls(prefixCls, className, {
454
- [`${prefixCls}-focus`]: focusing,
549
+ [`${prefixCls}-focus`]: focusing || active,
455
550
  [`${prefixCls}-disabled`]: disabled,
456
551
  [`${prefixCls}-hover`]: hovering && !disabled,
457
552
  [`${prefixCls}-error`]: validateStatus === 'error',
@@ -463,7 +558,9 @@ class TagInput extends BaseComponent<TagInputProps, TagInputState> {
463
558
  const wrapperCls = cls(`${prefixCls}-wrapper`);
464
559
 
465
560
  return (
561
+ // eslint-disable-next-line
466
562
  <div
563
+ ref={this.tagInputRef}
467
564
  style={style}
468
565
  className={tagInputCls}
469
566
  aria-disabled={disabled}
@@ -475,6 +572,9 @@ class TagInput extends BaseComponent<TagInputProps, TagInputState> {
475
572
  onMouseLeave={e => {
476
573
  this.handleInputMouseLeave(e);
477
574
  }}
575
+ onClick={e => {
576
+ this.handleClick(e);
577
+ }}
478
578
  >
479
579
  {this.renderPrefix()}
480
580
  <div className={wrapperCls}>