@rc-component/select 1.0.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/LICENSE.md +9 -0
- package/README.md +191 -0
- package/assets/index.css +306 -0
- package/assets/index.less +397 -0
- package/es/BaseSelect/Polite.d.ts +7 -0
- package/es/BaseSelect/Polite.js +26 -0
- package/es/BaseSelect/index.d.ts +118 -0
- package/es/BaseSelect/index.js +569 -0
- package/es/OptGroup.d.ts +12 -0
- package/es/OptGroup.js +6 -0
- package/es/Option.d.ts +14 -0
- package/es/Option.js +6 -0
- package/es/OptionList.d.ts +10 -0
- package/es/OptionList.js +379 -0
- package/es/Select.d.ts +114 -0
- package/es/Select.js +480 -0
- package/es/SelectContext.d.ts +23 -0
- package/es/SelectContext.js +6 -0
- package/es/SelectTrigger.d.ts +30 -0
- package/es/SelectTrigger.js +138 -0
- package/es/Selector/Input.d.ts +27 -0
- package/es/Selector/Input.js +114 -0
- package/es/Selector/MultipleSelector.d.ts +16 -0
- package/es/Selector/MultipleSelector.js +185 -0
- package/es/Selector/SingleSelector.d.ts +8 -0
- package/es/Selector/SingleSelector.js +104 -0
- package/es/Selector/index.d.ts +85 -0
- package/es/Selector/index.js +184 -0
- package/es/TransBtn.d.ts +12 -0
- package/es/TransBtn.js +30 -0
- package/es/hooks/useAllowClear.d.ts +8 -0
- package/es/hooks/useAllowClear.js +26 -0
- package/es/hooks/useBaseProps.d.ts +13 -0
- package/es/hooks/useBaseProps.js +10 -0
- package/es/hooks/useCache.d.ts +7 -0
- package/es/hooks/useCache.js +40 -0
- package/es/hooks/useDelayReset.d.ts +5 -0
- package/es/hooks/useDelayReset.js +24 -0
- package/es/hooks/useFilterOptions.d.ts +3 -0
- package/es/hooks/useFilterOptions.js +57 -0
- package/es/hooks/useId.d.ts +5 -0
- package/es/hooks/useId.js +29 -0
- package/es/hooks/useLayoutEffect.d.ts +5 -0
- package/es/hooks/useLayoutEffect.js +17 -0
- package/es/hooks/useLock.d.ts +7 -0
- package/es/hooks/useLock.js +27 -0
- package/es/hooks/useOptions.d.ts +12 -0
- package/es/hooks/useOptions.js +45 -0
- package/es/hooks/useRefFunc.d.ts +5 -0
- package/es/hooks/useRefFunc.js +14 -0
- package/es/hooks/useSelectTriggerControl.d.ts +1 -0
- package/es/hooks/useSelectTriggerControl.js +27 -0
- package/es/index.d.ts +10 -0
- package/es/index.js +7 -0
- package/es/interface.d.ts +23 -0
- package/es/interface.js +1 -0
- package/es/utils/__mocks__/platformUtil.d.ts +1 -0
- package/es/utils/__mocks__/platformUtil.js +3 -0
- package/es/utils/commonUtil.d.ts +9 -0
- package/es/utils/commonUtil.js +32 -0
- package/es/utils/keyUtil.d.ts +2 -0
- package/es/utils/keyUtil.js +16 -0
- package/es/utils/legacyUtil.d.ts +3 -0
- package/es/utils/legacyUtil.js +44 -0
- package/es/utils/platformUtil.d.ts +1 -0
- package/es/utils/platformUtil.js +4 -0
- package/es/utils/valueUtil.d.ts +24 -0
- package/es/utils/valueUtil.js +128 -0
- package/es/utils/warningPropsUtil.d.ts +4 -0
- package/es/utils/warningPropsUtil.js +119 -0
- package/lib/BaseSelect/Polite.d.ts +7 -0
- package/lib/BaseSelect/Polite.js +34 -0
- package/lib/BaseSelect/index.d.ts +118 -0
- package/lib/BaseSelect/index.js +579 -0
- package/lib/OptGroup.d.ts +12 -0
- package/lib/OptGroup.js +12 -0
- package/lib/Option.d.ts +14 -0
- package/lib/Option.js +12 -0
- package/lib/OptionList.d.ts +10 -0
- package/lib/OptionList.js +387 -0
- package/lib/Select.d.ts +114 -0
- package/lib/Select.js +487 -0
- package/lib/SelectContext.d.ts +23 -0
- package/lib/SelectContext.js +13 -0
- package/lib/SelectTrigger.d.ts +30 -0
- package/lib/SelectTrigger.js +147 -0
- package/lib/Selector/Input.d.ts +27 -0
- package/lib/Selector/Input.js +123 -0
- package/lib/Selector/MultipleSelector.d.ts +16 -0
- package/lib/Selector/MultipleSelector.js +194 -0
- package/lib/Selector/SingleSelector.d.ts +8 -0
- package/lib/Selector/SingleSelector.js +113 -0
- package/lib/Selector/index.d.ts +85 -0
- package/lib/Selector/index.js +191 -0
- package/lib/TransBtn.d.ts +12 -0
- package/lib/TransBtn.js +39 -0
- package/lib/hooks/useAllowClear.d.ts +8 -0
- package/lib/hooks/useAllowClear.js +34 -0
- package/lib/hooks/useBaseProps.d.ts +13 -0
- package/lib/hooks/useBaseProps.js +19 -0
- package/lib/hooks/useCache.d.ts +7 -0
- package/lib/hooks/useCache.js +49 -0
- package/lib/hooks/useDelayReset.d.ts +5 -0
- package/lib/hooks/useDelayReset.js +31 -0
- package/lib/hooks/useFilterOptions.d.ts +3 -0
- package/lib/hooks/useFilterOptions.js +66 -0
- package/lib/hooks/useId.d.ts +5 -0
- package/lib/hooks/useId.js +40 -0
- package/lib/hooks/useLayoutEffect.d.ts +5 -0
- package/lib/hooks/useLayoutEffect.js +25 -0
- package/lib/hooks/useLock.d.ts +7 -0
- package/lib/hooks/useLock.js +34 -0
- package/lib/hooks/useOptions.d.ts +12 -0
- package/lib/hooks/useOptions.js +52 -0
- package/lib/hooks/useRefFunc.d.ts +5 -0
- package/lib/hooks/useRefFunc.js +21 -0
- package/lib/hooks/useSelectTriggerControl.d.ts +1 -0
- package/lib/hooks/useSelectTriggerControl.js +35 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +37 -0
- package/lib/interface.d.ts +23 -0
- package/lib/interface.js +5 -0
- package/lib/utils/__mocks__/platformUtil.d.ts +1 -0
- package/lib/utils/__mocks__/platformUtil.js +9 -0
- package/lib/utils/commonUtil.d.ts +9 -0
- package/lib/utils/commonUtil.js +42 -0
- package/lib/utils/keyUtil.d.ts +2 -0
- package/lib/utils/keyUtil.js +22 -0
- package/lib/utils/legacyUtil.d.ts +3 -0
- package/lib/utils/legacyUtil.js +53 -0
- package/lib/utils/platformUtil.d.ts +1 -0
- package/lib/utils/platformUtil.js +10 -0
- package/lib/utils/valueUtil.d.ts +24 -0
- package/lib/utils/valueUtil.js +140 -0
- package/lib/utils/warningPropsUtil.d.ts +4 -0
- package/lib/utils/warningPropsUtil.js +129 -0
- package/package.json +86 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import useLayoutEffect from "@rc-component/util/es/hooks/useLayoutEffect";
|
|
4
|
+
import useMergedState from "@rc-component/util/es/hooks/useMergedState";
|
|
5
|
+
import isMobile from "@rc-component/util/es/isMobile";
|
|
6
|
+
import { useComposeRef } from "@rc-component/util/es/ref";
|
|
7
|
+
import * as React from 'react';
|
|
8
|
+
import { useAllowClear } from "../hooks/useAllowClear";
|
|
9
|
+
import { BaseSelectContext } from "../hooks/useBaseProps";
|
|
10
|
+
import useDelayReset from "../hooks/useDelayReset";
|
|
11
|
+
import useLock from "../hooks/useLock";
|
|
12
|
+
import useSelectTriggerControl from "../hooks/useSelectTriggerControl";
|
|
13
|
+
import Selector from "../Selector";
|
|
14
|
+
import SelectTrigger from "../SelectTrigger";
|
|
15
|
+
import TransBtn from "../TransBtn";
|
|
16
|
+
import { getSeparatedContent, isValidCount } from "../utils/valueUtil";
|
|
17
|
+
import SelectContext from "../SelectContext";
|
|
18
|
+
import Polite from "./Polite";
|
|
19
|
+
const DEFAULT_OMIT_PROPS = ['value', 'onChange', 'removeIcon', 'placeholder', 'autoFocus', 'maxTagCount', 'maxTagTextLength', 'maxTagPlaceholder', 'choiceTransitionName', 'onInputKeyDown', 'onPopupScroll', 'tabIndex'];
|
|
20
|
+
export const isMultiple = mode => mode === 'tags' || mode === 'multiple';
|
|
21
|
+
const BaseSelect = /*#__PURE__*/React.forwardRef((props, ref) => {
|
|
22
|
+
const {
|
|
23
|
+
id,
|
|
24
|
+
prefixCls,
|
|
25
|
+
className,
|
|
26
|
+
showSearch,
|
|
27
|
+
tagRender,
|
|
28
|
+
showScrollBar = 'optional',
|
|
29
|
+
direction,
|
|
30
|
+
omitDomProps,
|
|
31
|
+
// Value
|
|
32
|
+
displayValues,
|
|
33
|
+
onDisplayValuesChange,
|
|
34
|
+
emptyOptions,
|
|
35
|
+
notFoundContent = 'Not Found',
|
|
36
|
+
onClear,
|
|
37
|
+
// Mode
|
|
38
|
+
mode,
|
|
39
|
+
// Status
|
|
40
|
+
disabled,
|
|
41
|
+
loading,
|
|
42
|
+
// Customize Input
|
|
43
|
+
getInputElement,
|
|
44
|
+
getRawInputElement,
|
|
45
|
+
// Open
|
|
46
|
+
open,
|
|
47
|
+
defaultOpen,
|
|
48
|
+
onPopupVisibleChange,
|
|
49
|
+
// Active
|
|
50
|
+
activeValue,
|
|
51
|
+
onActiveValueChange,
|
|
52
|
+
activeDescendantId,
|
|
53
|
+
// Search
|
|
54
|
+
searchValue,
|
|
55
|
+
autoClearSearchValue,
|
|
56
|
+
onSearch,
|
|
57
|
+
onSearchSplit,
|
|
58
|
+
tokenSeparators,
|
|
59
|
+
// Icons
|
|
60
|
+
allowClear,
|
|
61
|
+
prefix,
|
|
62
|
+
suffixIcon,
|
|
63
|
+
clearIcon,
|
|
64
|
+
// Dropdown
|
|
65
|
+
OptionList,
|
|
66
|
+
animation,
|
|
67
|
+
transitionName,
|
|
68
|
+
popupStyle,
|
|
69
|
+
popupClassName,
|
|
70
|
+
popupMatchSelectWidth,
|
|
71
|
+
popupRender,
|
|
72
|
+
popupAlign,
|
|
73
|
+
placement,
|
|
74
|
+
builtinPlacements,
|
|
75
|
+
getPopupContainer,
|
|
76
|
+
// Focus
|
|
77
|
+
showAction = [],
|
|
78
|
+
onFocus,
|
|
79
|
+
onBlur,
|
|
80
|
+
// Rest Events
|
|
81
|
+
onKeyUp,
|
|
82
|
+
onKeyDown,
|
|
83
|
+
onMouseDown,
|
|
84
|
+
// Rest Props
|
|
85
|
+
...restProps
|
|
86
|
+
} = props;
|
|
87
|
+
|
|
88
|
+
// ============================== MISC ==============================
|
|
89
|
+
const multiple = isMultiple(mode);
|
|
90
|
+
const mergedShowSearch = (showSearch !== undefined ? showSearch : multiple) || mode === 'combobox';
|
|
91
|
+
const domProps = {
|
|
92
|
+
...restProps
|
|
93
|
+
};
|
|
94
|
+
DEFAULT_OMIT_PROPS.forEach(propName => {
|
|
95
|
+
delete domProps[propName];
|
|
96
|
+
});
|
|
97
|
+
omitDomProps?.forEach(propName => {
|
|
98
|
+
delete domProps[propName];
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ============================= Mobile =============================
|
|
102
|
+
const [mobile, setMobile] = React.useState(false);
|
|
103
|
+
React.useEffect(() => {
|
|
104
|
+
// Only update on the client side
|
|
105
|
+
setMobile(isMobile());
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
// ============================== Refs ==============================
|
|
109
|
+
const containerRef = React.useRef(null);
|
|
110
|
+
const selectorDomRef = React.useRef(null);
|
|
111
|
+
const triggerRef = React.useRef(null);
|
|
112
|
+
const selectorRef = React.useRef(null);
|
|
113
|
+
const listRef = React.useRef(null);
|
|
114
|
+
const blurRef = React.useRef(false);
|
|
115
|
+
|
|
116
|
+
/** Used for component focused management */
|
|
117
|
+
const [mockFocused, setMockFocused, cancelSetMockFocused] = useDelayReset();
|
|
118
|
+
|
|
119
|
+
// =========================== Imperative ===========================
|
|
120
|
+
React.useImperativeHandle(ref, () => ({
|
|
121
|
+
focus: selectorRef.current?.focus,
|
|
122
|
+
blur: selectorRef.current?.blur,
|
|
123
|
+
scrollTo: arg => listRef.current?.scrollTo(arg),
|
|
124
|
+
nativeElement: containerRef.current || selectorDomRef.current
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
// ========================== Search Value ==========================
|
|
128
|
+
const mergedSearchValue = React.useMemo(() => {
|
|
129
|
+
if (mode !== 'combobox') {
|
|
130
|
+
return searchValue;
|
|
131
|
+
}
|
|
132
|
+
const val = displayValues[0]?.value;
|
|
133
|
+
return typeof val === 'string' || typeof val === 'number' ? String(val) : '';
|
|
134
|
+
}, [searchValue, mode, displayValues]);
|
|
135
|
+
|
|
136
|
+
// ========================== Custom Input ==========================
|
|
137
|
+
// Only works in `combobox`
|
|
138
|
+
const customizeInputElement = mode === 'combobox' && typeof getInputElement === 'function' && getInputElement() || null;
|
|
139
|
+
|
|
140
|
+
// Used for customize replacement for `rc-cascader`
|
|
141
|
+
const customizeRawInputElement = typeof getRawInputElement === 'function' && getRawInputElement();
|
|
142
|
+
const customizeRawInputRef = useComposeRef(selectorDomRef, customizeRawInputElement?.props?.ref);
|
|
143
|
+
|
|
144
|
+
// ============================== Open ==============================
|
|
145
|
+
// SSR not support Portal which means we need delay `open` for the first time render
|
|
146
|
+
const [rendered, setRendered] = React.useState(false);
|
|
147
|
+
useLayoutEffect(() => {
|
|
148
|
+
setRendered(true);
|
|
149
|
+
}, []);
|
|
150
|
+
const [innerOpen, setInnerOpen] = useMergedState(false, {
|
|
151
|
+
defaultValue: defaultOpen,
|
|
152
|
+
value: open
|
|
153
|
+
});
|
|
154
|
+
let mergedOpen = rendered ? innerOpen : false;
|
|
155
|
+
|
|
156
|
+
// Not trigger `open` in `combobox` when `notFoundContent` is empty
|
|
157
|
+
const emptyListContent = !notFoundContent && emptyOptions;
|
|
158
|
+
if (disabled || emptyListContent && mergedOpen && mode === 'combobox') {
|
|
159
|
+
mergedOpen = false;
|
|
160
|
+
}
|
|
161
|
+
const triggerOpen = emptyListContent ? false : mergedOpen;
|
|
162
|
+
const onToggleOpen = React.useCallback(newOpen => {
|
|
163
|
+
const nextOpen = newOpen !== undefined ? newOpen : !mergedOpen;
|
|
164
|
+
if (!disabled) {
|
|
165
|
+
setInnerOpen(nextOpen);
|
|
166
|
+
if (mergedOpen !== nextOpen) {
|
|
167
|
+
onPopupVisibleChange?.(nextOpen);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}, [disabled, mergedOpen, setInnerOpen, onPopupVisibleChange]);
|
|
171
|
+
|
|
172
|
+
// ============================= Search =============================
|
|
173
|
+
const tokenWithEnter = React.useMemo(() => (tokenSeparators || []).some(tokenSeparator => ['\n', '\r\n'].includes(tokenSeparator)), [tokenSeparators]);
|
|
174
|
+
const {
|
|
175
|
+
maxCount,
|
|
176
|
+
rawValues
|
|
177
|
+
} = React.useContext(SelectContext) || {};
|
|
178
|
+
const onInternalSearch = (searchText, fromTyping, isCompositing) => {
|
|
179
|
+
if (multiple && isValidCount(maxCount) && rawValues?.size >= maxCount) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
let ret = true;
|
|
183
|
+
let newSearchText = searchText;
|
|
184
|
+
onActiveValueChange?.(null);
|
|
185
|
+
const separatedList = getSeparatedContent(searchText, tokenSeparators, isValidCount(maxCount) ? maxCount - rawValues.size : undefined);
|
|
186
|
+
|
|
187
|
+
// Check if match the `tokenSeparators`
|
|
188
|
+
const patchLabels = isCompositing ? null : separatedList;
|
|
189
|
+
|
|
190
|
+
// Ignore combobox since it's not split-able
|
|
191
|
+
if (mode !== 'combobox' && patchLabels) {
|
|
192
|
+
newSearchText = '';
|
|
193
|
+
onSearchSplit?.(patchLabels);
|
|
194
|
+
|
|
195
|
+
// Should close when paste finish
|
|
196
|
+
onToggleOpen(false);
|
|
197
|
+
|
|
198
|
+
// Tell Selector that break next actions
|
|
199
|
+
ret = false;
|
|
200
|
+
}
|
|
201
|
+
if (onSearch && mergedSearchValue !== newSearchText) {
|
|
202
|
+
onSearch(newSearchText, {
|
|
203
|
+
source: fromTyping ? 'typing' : 'effect'
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return ret;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Only triggered when menu is closed & mode is tags
|
|
210
|
+
// If menu is open, OptionList will take charge
|
|
211
|
+
// If mode isn't tags, press enter is not meaningful when you can't see any option
|
|
212
|
+
const onInternalSearchSubmit = searchText => {
|
|
213
|
+
// prevent empty tags from appearing when you click the Enter button
|
|
214
|
+
if (!searchText || !searchText.trim()) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
onSearch(searchText, {
|
|
218
|
+
source: 'submit'
|
|
219
|
+
});
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Close will clean up single mode search text
|
|
223
|
+
React.useEffect(() => {
|
|
224
|
+
if (!mergedOpen && !multiple && mode !== 'combobox') {
|
|
225
|
+
onInternalSearch('', false, false);
|
|
226
|
+
}
|
|
227
|
+
}, [mergedOpen]);
|
|
228
|
+
|
|
229
|
+
// ============================ Disabled ============================
|
|
230
|
+
// Close dropdown & remove focus state when disabled change
|
|
231
|
+
React.useEffect(() => {
|
|
232
|
+
if (innerOpen && disabled) {
|
|
233
|
+
setInnerOpen(false);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// After onBlur is triggered, the focused does not need to be reset
|
|
237
|
+
if (disabled && !blurRef.current) {
|
|
238
|
+
setMockFocused(false);
|
|
239
|
+
}
|
|
240
|
+
}, [disabled]);
|
|
241
|
+
|
|
242
|
+
// ============================ Keyboard ============================
|
|
243
|
+
/**
|
|
244
|
+
* We record input value here to check if can press to clean up by backspace
|
|
245
|
+
* - null: Key is not down, this is reset by key up
|
|
246
|
+
* - true: Search text is empty when first time backspace down
|
|
247
|
+
* - false: Search text is not empty when first time backspace down
|
|
248
|
+
*/
|
|
249
|
+
const [getClearLock, setClearLock] = useLock();
|
|
250
|
+
const keyLockRef = React.useRef(false);
|
|
251
|
+
|
|
252
|
+
// KeyDown
|
|
253
|
+
const onInternalKeyDown = (event, ...rest) => {
|
|
254
|
+
const clearLock = getClearLock();
|
|
255
|
+
const {
|
|
256
|
+
key
|
|
257
|
+
} = event;
|
|
258
|
+
const isEnterKey = key === 'Enter';
|
|
259
|
+
if (isEnterKey) {
|
|
260
|
+
// Do not submit form when type in the input
|
|
261
|
+
if (mode !== 'combobox') {
|
|
262
|
+
event.preventDefault();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// We only manage open state here, close logic should handle by list component
|
|
266
|
+
if (!mergedOpen) {
|
|
267
|
+
onToggleOpen(true);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
setClearLock(!!mergedSearchValue);
|
|
271
|
+
|
|
272
|
+
// Remove value by `backspace`
|
|
273
|
+
if (key === 'Backspace' && !clearLock && multiple && !mergedSearchValue && displayValues.length) {
|
|
274
|
+
const cloneDisplayValues = [...displayValues];
|
|
275
|
+
let removedDisplayValue = null;
|
|
276
|
+
for (let i = cloneDisplayValues.length - 1; i >= 0; i -= 1) {
|
|
277
|
+
const current = cloneDisplayValues[i];
|
|
278
|
+
if (!current.disabled) {
|
|
279
|
+
cloneDisplayValues.splice(i, 1);
|
|
280
|
+
removedDisplayValue = current;
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (removedDisplayValue) {
|
|
285
|
+
onDisplayValuesChange(cloneDisplayValues, {
|
|
286
|
+
type: 'remove',
|
|
287
|
+
values: [removedDisplayValue]
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (mergedOpen && (!isEnterKey || !keyLockRef.current)) {
|
|
292
|
+
// Lock the Enter key after it is pressed to avoid repeated triggering of the onChange event.
|
|
293
|
+
if (isEnterKey) {
|
|
294
|
+
keyLockRef.current = true;
|
|
295
|
+
}
|
|
296
|
+
listRef.current?.onKeyDown(event, ...rest);
|
|
297
|
+
}
|
|
298
|
+
onKeyDown?.(event, ...rest);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// KeyUp
|
|
302
|
+
const onInternalKeyUp = (event, ...rest) => {
|
|
303
|
+
if (mergedOpen) {
|
|
304
|
+
listRef.current?.onKeyUp(event, ...rest);
|
|
305
|
+
}
|
|
306
|
+
if (event.key === 'Enter') {
|
|
307
|
+
keyLockRef.current = false;
|
|
308
|
+
}
|
|
309
|
+
onKeyUp?.(event, ...rest);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// ============================ Selector ============================
|
|
313
|
+
const onSelectorRemove = val => {
|
|
314
|
+
const newValues = displayValues.filter(i => i !== val);
|
|
315
|
+
onDisplayValuesChange(newValues, {
|
|
316
|
+
type: 'remove',
|
|
317
|
+
values: [val]
|
|
318
|
+
});
|
|
319
|
+
};
|
|
320
|
+
const onInputBlur = () => {
|
|
321
|
+
// Unlock the Enter key after the input blur; otherwise, the Enter key needs to be pressed twice to trigger the correct effect.
|
|
322
|
+
keyLockRef.current = false;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// ========================== Focus / Blur ==========================
|
|
326
|
+
/** Record real focus status */
|
|
327
|
+
const focusRef = React.useRef(false);
|
|
328
|
+
const onContainerFocus = (...args) => {
|
|
329
|
+
setMockFocused(true);
|
|
330
|
+
if (!disabled) {
|
|
331
|
+
if (onFocus && !focusRef.current) {
|
|
332
|
+
onFocus(...args);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// `showAction` should handle `focus` if set
|
|
336
|
+
if (showAction.includes('focus')) {
|
|
337
|
+
onToggleOpen(true);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
focusRef.current = true;
|
|
341
|
+
};
|
|
342
|
+
const onContainerBlur = (...args) => {
|
|
343
|
+
blurRef.current = true;
|
|
344
|
+
setMockFocused(false, () => {
|
|
345
|
+
focusRef.current = false;
|
|
346
|
+
blurRef.current = false;
|
|
347
|
+
onToggleOpen(false);
|
|
348
|
+
});
|
|
349
|
+
if (disabled) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (mergedSearchValue) {
|
|
353
|
+
// `tags` mode should move `searchValue` into values
|
|
354
|
+
if (mode === 'tags') {
|
|
355
|
+
onSearch(mergedSearchValue, {
|
|
356
|
+
source: 'submit'
|
|
357
|
+
});
|
|
358
|
+
} else if (mode === 'multiple') {
|
|
359
|
+
// `multiple` mode only clean the search value but not trigger event
|
|
360
|
+
onSearch('', {
|
|
361
|
+
source: 'blur'
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (onBlur) {
|
|
366
|
+
onBlur(...args);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// Give focus back of Select
|
|
371
|
+
const activeTimeoutIds = [];
|
|
372
|
+
React.useEffect(() => () => {
|
|
373
|
+
activeTimeoutIds.forEach(timeoutId => clearTimeout(timeoutId));
|
|
374
|
+
activeTimeoutIds.splice(0, activeTimeoutIds.length);
|
|
375
|
+
}, []);
|
|
376
|
+
const onInternalMouseDown = (event, ...restArgs) => {
|
|
377
|
+
const {
|
|
378
|
+
target
|
|
379
|
+
} = event;
|
|
380
|
+
const popupElement = triggerRef.current?.getPopupElement();
|
|
381
|
+
|
|
382
|
+
// We should give focus back to selector if clicked item is not focusable
|
|
383
|
+
if (popupElement && popupElement.contains(target)) {
|
|
384
|
+
const timeoutId = setTimeout(() => {
|
|
385
|
+
const index = activeTimeoutIds.indexOf(timeoutId);
|
|
386
|
+
if (index !== -1) {
|
|
387
|
+
activeTimeoutIds.splice(index, 1);
|
|
388
|
+
}
|
|
389
|
+
cancelSetMockFocused();
|
|
390
|
+
if (!mobile && !popupElement.contains(document.activeElement)) {
|
|
391
|
+
selectorRef.current?.focus();
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
activeTimeoutIds.push(timeoutId);
|
|
395
|
+
}
|
|
396
|
+
onMouseDown?.(event, ...restArgs);
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// ============================ Dropdown ============================
|
|
400
|
+
const [, forceUpdate] = React.useState({});
|
|
401
|
+
// We need force update here since popup dom is render async
|
|
402
|
+
function onPopupMouseEnter() {
|
|
403
|
+
forceUpdate({});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Used for raw custom input trigger
|
|
407
|
+
let onTriggerVisibleChange;
|
|
408
|
+
if (customizeRawInputElement) {
|
|
409
|
+
onTriggerVisibleChange = newOpen => {
|
|
410
|
+
onToggleOpen(newOpen);
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Close when click on non-select element
|
|
415
|
+
useSelectTriggerControl(() => [containerRef.current, triggerRef.current?.getPopupElement()], triggerOpen, onToggleOpen, !!customizeRawInputElement);
|
|
416
|
+
|
|
417
|
+
// ============================ Context =============================
|
|
418
|
+
const baseSelectContext = React.useMemo(() => ({
|
|
419
|
+
...props,
|
|
420
|
+
notFoundContent,
|
|
421
|
+
open: mergedOpen,
|
|
422
|
+
triggerOpen,
|
|
423
|
+
id,
|
|
424
|
+
showSearch: mergedShowSearch,
|
|
425
|
+
multiple,
|
|
426
|
+
toggleOpen: onToggleOpen,
|
|
427
|
+
showScrollBar
|
|
428
|
+
}), [props, notFoundContent, triggerOpen, mergedOpen, id, mergedShowSearch, multiple, onToggleOpen, showScrollBar]);
|
|
429
|
+
|
|
430
|
+
// ==================================================================
|
|
431
|
+
// == Render ==
|
|
432
|
+
// ==================================================================
|
|
433
|
+
|
|
434
|
+
// ============================= Arrow ==============================
|
|
435
|
+
const showSuffixIcon = !!suffixIcon || loading;
|
|
436
|
+
let arrowNode;
|
|
437
|
+
if (showSuffixIcon) {
|
|
438
|
+
arrowNode = /*#__PURE__*/React.createElement(TransBtn, {
|
|
439
|
+
className: classNames(`${prefixCls}-arrow`, {
|
|
440
|
+
[`${prefixCls}-arrow-loading`]: loading
|
|
441
|
+
}),
|
|
442
|
+
customizeIcon: suffixIcon,
|
|
443
|
+
customizeIconProps: {
|
|
444
|
+
loading,
|
|
445
|
+
searchValue: mergedSearchValue,
|
|
446
|
+
open: mergedOpen,
|
|
447
|
+
focused: mockFocused,
|
|
448
|
+
showSearch: mergedShowSearch
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ============================= Clear ==============================
|
|
454
|
+
const onClearMouseDown = () => {
|
|
455
|
+
onClear?.();
|
|
456
|
+
selectorRef.current?.focus();
|
|
457
|
+
onDisplayValuesChange([], {
|
|
458
|
+
type: 'clear',
|
|
459
|
+
values: displayValues
|
|
460
|
+
});
|
|
461
|
+
onInternalSearch('', false, false);
|
|
462
|
+
};
|
|
463
|
+
const {
|
|
464
|
+
allowClear: mergedAllowClear,
|
|
465
|
+
clearIcon: clearNode
|
|
466
|
+
} = useAllowClear(prefixCls, onClearMouseDown, displayValues, allowClear, clearIcon, disabled, mergedSearchValue, mode);
|
|
467
|
+
|
|
468
|
+
// =========================== OptionList ===========================
|
|
469
|
+
const optionList = /*#__PURE__*/React.createElement(OptionList, {
|
|
470
|
+
ref: listRef
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// ============================= Select =============================
|
|
474
|
+
const mergedClassName = classNames(prefixCls, className, {
|
|
475
|
+
[`${prefixCls}-focused`]: mockFocused,
|
|
476
|
+
[`${prefixCls}-multiple`]: multiple,
|
|
477
|
+
[`${prefixCls}-single`]: !multiple,
|
|
478
|
+
[`${prefixCls}-allow-clear`]: allowClear,
|
|
479
|
+
[`${prefixCls}-show-arrow`]: showSuffixIcon,
|
|
480
|
+
[`${prefixCls}-disabled`]: disabled,
|
|
481
|
+
[`${prefixCls}-loading`]: loading,
|
|
482
|
+
[`${prefixCls}-open`]: mergedOpen,
|
|
483
|
+
[`${prefixCls}-customize-input`]: customizeInputElement,
|
|
484
|
+
[`${prefixCls}-show-search`]: mergedShowSearch
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// >>> Selector
|
|
488
|
+
const selectorNode = /*#__PURE__*/React.createElement(SelectTrigger, {
|
|
489
|
+
ref: triggerRef,
|
|
490
|
+
disabled: disabled,
|
|
491
|
+
prefixCls: prefixCls,
|
|
492
|
+
visible: triggerOpen,
|
|
493
|
+
popupElement: optionList,
|
|
494
|
+
animation: animation,
|
|
495
|
+
transitionName: transitionName,
|
|
496
|
+
popupStyle: popupStyle,
|
|
497
|
+
popupClassName: popupClassName,
|
|
498
|
+
direction: direction,
|
|
499
|
+
popupMatchSelectWidth: popupMatchSelectWidth,
|
|
500
|
+
popupRender: popupRender,
|
|
501
|
+
popupAlign: popupAlign,
|
|
502
|
+
placement: placement,
|
|
503
|
+
builtinPlacements: builtinPlacements,
|
|
504
|
+
getPopupContainer: getPopupContainer,
|
|
505
|
+
empty: emptyOptions,
|
|
506
|
+
getTriggerDOMNode: node =>
|
|
507
|
+
// TODO: This is workaround and should be removed in `rc-select`
|
|
508
|
+
// And use new standard `nativeElement` for ref.
|
|
509
|
+
// But we should update `rc-resize-observer` first.
|
|
510
|
+
selectorDomRef.current || node,
|
|
511
|
+
onPopupVisibleChange: onTriggerVisibleChange,
|
|
512
|
+
onPopupMouseEnter: onPopupMouseEnter
|
|
513
|
+
}, customizeRawInputElement ? ( /*#__PURE__*/React.cloneElement(customizeRawInputElement, {
|
|
514
|
+
ref: customizeRawInputRef
|
|
515
|
+
})) : /*#__PURE__*/React.createElement(Selector, _extends({}, props, {
|
|
516
|
+
domRef: selectorDomRef,
|
|
517
|
+
prefixCls: prefixCls,
|
|
518
|
+
inputElement: customizeInputElement,
|
|
519
|
+
ref: selectorRef,
|
|
520
|
+
id: id,
|
|
521
|
+
prefix: prefix,
|
|
522
|
+
showSearch: mergedShowSearch,
|
|
523
|
+
autoClearSearchValue: autoClearSearchValue,
|
|
524
|
+
mode: mode,
|
|
525
|
+
activeDescendantId: activeDescendantId,
|
|
526
|
+
tagRender: tagRender,
|
|
527
|
+
values: displayValues,
|
|
528
|
+
open: mergedOpen,
|
|
529
|
+
onToggleOpen: onToggleOpen,
|
|
530
|
+
activeValue: activeValue,
|
|
531
|
+
searchValue: mergedSearchValue,
|
|
532
|
+
onSearch: onInternalSearch,
|
|
533
|
+
onSearchSubmit: onInternalSearchSubmit,
|
|
534
|
+
onRemove: onSelectorRemove,
|
|
535
|
+
tokenWithEnter: tokenWithEnter,
|
|
536
|
+
onInputBlur: onInputBlur
|
|
537
|
+
})));
|
|
538
|
+
|
|
539
|
+
// >>> Render
|
|
540
|
+
let renderNode;
|
|
541
|
+
|
|
542
|
+
// Render raw
|
|
543
|
+
if (customizeRawInputElement) {
|
|
544
|
+
renderNode = selectorNode;
|
|
545
|
+
} else {
|
|
546
|
+
renderNode = /*#__PURE__*/React.createElement("div", _extends({
|
|
547
|
+
className: mergedClassName
|
|
548
|
+
}, domProps, {
|
|
549
|
+
ref: containerRef,
|
|
550
|
+
onMouseDown: onInternalMouseDown,
|
|
551
|
+
onKeyDown: onInternalKeyDown,
|
|
552
|
+
onKeyUp: onInternalKeyUp,
|
|
553
|
+
onFocus: onContainerFocus,
|
|
554
|
+
onBlur: onContainerBlur
|
|
555
|
+
}), /*#__PURE__*/React.createElement(Polite, {
|
|
556
|
+
visible: mockFocused && !mergedOpen,
|
|
557
|
+
values: displayValues
|
|
558
|
+
}), selectorNode, arrowNode, mergedAllowClear && clearNode);
|
|
559
|
+
}
|
|
560
|
+
return /*#__PURE__*/React.createElement(BaseSelectContext.Provider, {
|
|
561
|
+
value: baseSelectContext
|
|
562
|
+
}, renderNode);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Set display name for dev
|
|
566
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
567
|
+
BaseSelect.displayName = 'BaseSelect';
|
|
568
|
+
}
|
|
569
|
+
export default BaseSelect;
|
package/es/OptGroup.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type * as React from 'react';
|
|
2
|
+
import type { DefaultOptionType } from './Select';
|
|
3
|
+
export interface OptGroupProps extends Omit<DefaultOptionType, 'options'> {
|
|
4
|
+
children?: React.ReactNode;
|
|
5
|
+
}
|
|
6
|
+
export interface OptionGroupFC extends React.FC<OptGroupProps> {
|
|
7
|
+
/** Legacy for check if is a Option Group */
|
|
8
|
+
isSelectOptGroup: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** This is a placeholder, not real render in dom */
|
|
11
|
+
declare const OptGroup: OptionGroupFC;
|
|
12
|
+
export default OptGroup;
|
package/es/OptGroup.js
ADDED
package/es/Option.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type * as React from 'react';
|
|
2
|
+
import type { DefaultOptionType } from './Select';
|
|
3
|
+
export interface OptionProps extends Omit<DefaultOptionType, 'label'> {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
/** Save for customize data */
|
|
6
|
+
[prop: string]: any;
|
|
7
|
+
}
|
|
8
|
+
export interface OptionFC extends React.FC<OptionProps> {
|
|
9
|
+
/** Legacy for check if is a Option Group */
|
|
10
|
+
isSelectOption: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** This is a placeholder, not real render in dom */
|
|
13
|
+
declare const Option: OptionFC;
|
|
14
|
+
export default Option;
|
package/es/Option.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ScrollConfig } from 'rc-virtual-list/lib/List';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
export type OptionListProps = Record<string, never>;
|
|
4
|
+
export interface RefOptionListProps {
|
|
5
|
+
onKeyDown: React.KeyboardEventHandler;
|
|
6
|
+
onKeyUp: React.KeyboardEventHandler;
|
|
7
|
+
scrollTo?: (args: number | ScrollConfig) => void;
|
|
8
|
+
}
|
|
9
|
+
declare const RefOptionList: React.ForwardRefExoticComponent<React.RefAttributes<RefOptionListProps>>;
|
|
10
|
+
export default RefOptionList;
|