@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.
- package/collapse/__test__/collapse.test.js +22 -2
- package/collapse/_story/accordion.stories.js +2 -2
- package/collapse/item.tsx +20 -6
- package/datePicker/__test__/datePicker.test.js +5 -5
- package/datePicker/_story/datePicker.stories.js +138 -22
- package/datePicker/datePicker.tsx +42 -7
- package/datePicker/monthsGrid.tsx +22 -10
- package/datePicker/quickControl.tsx +62 -16
- package/datePicker/yearAndMonth.tsx +31 -5
- package/dist/css/semi.css +289 -32
- package/dist/css/semi.min.css +1 -1
- package/dist/umd/semi-ui.js +45706 -45192
- package/dist/umd/semi-ui.js.map +1 -1
- package/dist/umd/semi-ui.min.js +1 -1
- package/dist/umd/semi-ui.min.js.map +1 -1
- package/lib/cjs/collapse/item.d.ts +8 -0
- package/lib/cjs/collapse/item.js +19 -8
- package/lib/cjs/datePicker/datePicker.d.ts +3 -0
- package/lib/cjs/datePicker/datePicker.js +56 -9
- package/lib/cjs/datePicker/monthsGrid.d.ts +3 -0
- package/lib/cjs/datePicker/monthsGrid.js +14 -3
- package/lib/cjs/datePicker/quickControl.d.ts +6 -0
- package/lib/cjs/datePicker/quickControl.js +61 -14
- package/lib/cjs/datePicker/yearAndMonth.d.ts +3 -0
- package/lib/cjs/datePicker/yearAndMonth.js +15 -3
- package/lib/cjs/popover/index.d.ts +3 -0
- package/lib/cjs/popover/index.js +4 -2
- package/lib/cjs/select/index.d.ts +6 -1
- package/lib/cjs/select/index.js +130 -72
- package/lib/cjs/select/option.js +4 -2
- package/lib/cjs/tag/index.js +6 -4
- package/lib/cjs/tag/interface.d.ts +1 -0
- package/lib/cjs/tagInput/index.d.ts +13 -1
- package/lib/cjs/tagInput/index.js +217 -91
- package/lib/cjs/tooltip/index.d.ts +4 -0
- package/lib/cjs/tooltip/index.js +5 -3
- package/lib/es/collapse/item.d.ts +8 -0
- package/lib/es/collapse/item.js +19 -8
- package/lib/es/datePicker/datePicker.d.ts +3 -0
- package/lib/es/datePicker/datePicker.js +56 -9
- package/lib/es/datePicker/monthsGrid.d.ts +3 -0
- package/lib/es/datePicker/monthsGrid.js +14 -3
- package/lib/es/datePicker/quickControl.d.ts +6 -0
- package/lib/es/datePicker/quickControl.js +61 -15
- package/lib/es/datePicker/yearAndMonth.d.ts +3 -0
- package/lib/es/datePicker/yearAndMonth.js +14 -3
- package/lib/es/popover/index.d.ts +3 -0
- package/lib/es/popover/index.js +4 -2
- package/lib/es/select/index.d.ts +6 -1
- package/lib/es/select/index.js +129 -71
- package/lib/es/select/option.js +4 -2
- package/lib/es/tag/index.js +6 -4
- package/lib/es/tag/interface.d.ts +1 -0
- package/lib/es/tagInput/index.d.ts +13 -1
- package/lib/es/tagInput/index.js +217 -93
- package/lib/es/tooltip/index.d.ts +4 -0
- package/lib/es/tooltip/index.js +5 -3
- package/package.json +7 -7
- package/popover/index.tsx +4 -1
- package/select/__test__/select.test.js +5 -3
- package/select/index.tsx +65 -30
- package/select/option.tsx +2 -0
- package/tag/index.tsx +3 -2
- package/tag/interface.ts +1 -0
- package/tagInput/_story/tagInput.stories.js +18 -0
- package/tagInput/index.tsx +126 -26
- 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:
|
|
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
|
-
|
|
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
|
-
{
|
|
1113
|
-
|
|
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
|
-
|
|
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=
|
|
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={
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/tagInput/index.tsx
CHANGED
|
@@ -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
|
-
|
|
395
|
+
getAllTags = () => {
|
|
342
396
|
const {
|
|
343
397
|
size,
|
|
344
398
|
disabled,
|
|
345
399
|
renderTagItem,
|
|
346
|
-
maxTagCount,
|
|
347
400
|
showContentTooltip,
|
|
348
|
-
|
|
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
|
|
359
|
-
[`${prefixCls}-
|
|
412
|
+
const itemWrapperCls = cls({
|
|
413
|
+
[`${prefixCls}-drag-item`]: showIconHandler,
|
|
414
|
+
[`${prefixCls}-wrapper-tag-icon`]: showIconHandler,
|
|
360
415
|
});
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
420
|
+
return showIconHandler? (<div className={itemWrapperCls} key={elementKey}>
|
|
421
|
+
<DragHandle />
|
|
422
|
+
{renderTagItem(value, index)}
|
|
423
|
+
</div>) : renderTagItem(value, index);
|
|
367
424
|
} else {
|
|
368
|
-
|
|
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={
|
|
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}>
|