UNPKG

21 kBJavaScriptView Raw
1import _extends from "@babel/runtime/helpers/esm/extends";
2import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray";
3import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
4import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
5import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
6import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
7import _typeof from "@babel/runtime/helpers/esm/typeof";
8var _excluded = ["id", "mode", "prefixCls", "backfill", "fieldNames", "inputValue", "searchValue", "onSearch", "autoClearSearchValue", "onSelect", "onDeselect", "dropdownMatchSelectWidth", "filterOption", "filterSort", "optionFilterProp", "optionLabelProp", "options", "children", "defaultActiveFirstOption", "menuItemSelectedIcon", "virtual", "direction", "listHeight", "listItemHeight", "value", "defaultValue", "labelInValue", "onChange"];
9/**
10 * To match accessibility requirement, we always provide an input in the component.
11 * Other element will not set `tabIndex` to avoid `onBlur` sequence problem.
12 * For focused select, we set `aria-live="polite"` to update the accessibility content.
13 *
14 * ref:
15 * - keyboard: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role#Keyboard_interactions
16 *
17 * New api:
18 * - listHeight
19 * - listItemHeight
20 * - component
21 *
22 * Remove deprecated api:
23 * - multiple
24 * - tags
25 * - combobox
26 * - firstActiveValue
27 * - dropdownMenuStyle
28 * - openClassName (Not list in api)
29 *
30 * Update:
31 * - `backfill` only support `combobox` mode
32 * - `combobox` mode not support `labelInValue` since it's meaningless
33 * - `getInputElement` only support `combobox` mode
34 * - `onChange` return OptionData instead of ReactNode
35 * - `filterOption` `onChange` `onSelect` accept OptionData instead of ReactNode
36 * - `combobox` mode trigger `onChange` will get `undefined` if no `value` match in Option
37 * - `combobox` mode not support `optionLabelProp`
38 */
39
40import useMergedState from "rc-util/es/hooks/useMergedState";
41import warning from "rc-util/es/warning";
42import * as React from 'react';
43import BaseSelect, { isMultiple } from "./BaseSelect";
44import useCache from "./hooks/useCache";
45import useFilterOptions from "./hooks/useFilterOptions";
46import useId from "./hooks/useId";
47import useOptions from "./hooks/useOptions";
48import useRefFunc from "./hooks/useRefFunc";
49import OptGroup from "./OptGroup";
50import Option from "./Option";
51import OptionList from "./OptionList";
52import SelectContext from "./SelectContext";
53import { hasValue, isComboNoValue, toArray } from "./utils/commonUtil";
54import { fillFieldNames, flattenOptions, injectPropsWithOption } from "./utils/valueUtil";
55import warningProps, { warningNullOptions } from "./utils/warningPropsUtil";
56var OMIT_DOM_PROPS = ['inputValue'];
57function isRawValue(value) {
58 return !value || _typeof(value) !== 'object';
59}
60var Select = /*#__PURE__*/React.forwardRef(function (props, ref) {
61 var id = props.id,
62 mode = props.mode,
63 _props$prefixCls = props.prefixCls,
64 prefixCls = _props$prefixCls === void 0 ? 'rc-select' : _props$prefixCls,
65 backfill = props.backfill,
66 fieldNames = props.fieldNames,
67 inputValue = props.inputValue,
68 searchValue = props.searchValue,
69 onSearch = props.onSearch,
70 _props$autoClearSearc = props.autoClearSearchValue,
71 autoClearSearchValue = _props$autoClearSearc === void 0 ? true : _props$autoClearSearc,
72 onSelect = props.onSelect,
73 onDeselect = props.onDeselect,
74 _props$dropdownMatchS = props.dropdownMatchSelectWidth,
75 dropdownMatchSelectWidth = _props$dropdownMatchS === void 0 ? true : _props$dropdownMatchS,
76 filterOption = props.filterOption,
77 filterSort = props.filterSort,
78 optionFilterProp = props.optionFilterProp,
79 optionLabelProp = props.optionLabelProp,
80 options = props.options,
81 children = props.children,
82 defaultActiveFirstOption = props.defaultActiveFirstOption,
83 menuItemSelectedIcon = props.menuItemSelectedIcon,
84 virtual = props.virtual,
85 direction = props.direction,
86 _props$listHeight = props.listHeight,
87 listHeight = _props$listHeight === void 0 ? 200 : _props$listHeight,
88 _props$listItemHeight = props.listItemHeight,
89 listItemHeight = _props$listItemHeight === void 0 ? 20 : _props$listItemHeight,
90 value = props.value,
91 defaultValue = props.defaultValue,
92 labelInValue = props.labelInValue,
93 onChange = props.onChange,
94 restProps = _objectWithoutProperties(props, _excluded);
95 var mergedId = useId(id);
96 var multiple = isMultiple(mode);
97 var childrenAsData = !!(!options && children);
98 var mergedFilterOption = React.useMemo(function () {
99 if (filterOption === undefined && mode === 'combobox') {
100 return false;
101 }
102 return filterOption;
103 }, [filterOption, mode]);
104
105 // ========================= FieldNames =========================
106 var mergedFieldNames = React.useMemo(function () {
107 return fillFieldNames(fieldNames, childrenAsData);
108 }, /* eslint-disable react-hooks/exhaustive-deps */
109 [
110 // We stringify fieldNames to avoid unnecessary re-renders.
111 JSON.stringify(fieldNames), childrenAsData]
112 /* eslint-enable react-hooks/exhaustive-deps */);
113
114 // =========================== Search ===========================
115 var _useMergedState = useMergedState('', {
116 value: searchValue !== undefined ? searchValue : inputValue,
117 postState: function postState(search) {
118 return search || '';
119 }
120 }),
121 _useMergedState2 = _slicedToArray(_useMergedState, 2),
122 mergedSearchValue = _useMergedState2[0],
123 setSearchValue = _useMergedState2[1];
124
125 // =========================== Option ===========================
126 var parsedOptions = useOptions(options, children, mergedFieldNames, optionFilterProp, optionLabelProp);
127 var valueOptions = parsedOptions.valueOptions,
128 labelOptions = parsedOptions.labelOptions,
129 mergedOptions = parsedOptions.options;
130
131 // ========================= Wrap Value =========================
132 var convert2LabelValues = React.useCallback(function (draftValues) {
133 // Convert to array
134 var valueList = toArray(draftValues);
135
136 // Convert to labelInValue type
137 return valueList.map(function (val) {
138 var rawValue;
139 var rawLabel;
140 var rawKey;
141 var rawDisabled;
142 var rawTitle;
143
144 // Fill label & value
145 if (isRawValue(val)) {
146 rawValue = val;
147 } else {
148 var _val$value;
149 rawKey = val.key;
150 rawLabel = val.label;
151 rawValue = (_val$value = val.value) !== null && _val$value !== void 0 ? _val$value : rawKey;
152 }
153 var option = valueOptions.get(rawValue);
154 if (option) {
155 var _option$key;
156 // Fill missing props
157 if (rawLabel === undefined) rawLabel = option === null || option === void 0 ? void 0 : option[optionLabelProp || mergedFieldNames.label];
158 if (rawKey === undefined) rawKey = (_option$key = option === null || option === void 0 ? void 0 : option.key) !== null && _option$key !== void 0 ? _option$key : rawValue;
159 rawDisabled = option === null || option === void 0 ? void 0 : option.disabled;
160 rawTitle = option === null || option === void 0 ? void 0 : option.title;
161
162 // Warning if label not same as provided
163 if (process.env.NODE_ENV !== 'production' && !optionLabelProp) {
164 var optionLabel = option === null || option === void 0 ? void 0 : option[mergedFieldNames.label];
165 if (optionLabel !== undefined && ! /*#__PURE__*/React.isValidElement(optionLabel) && ! /*#__PURE__*/React.isValidElement(rawLabel) && optionLabel !== rawLabel) {
166 warning(false, '`label` of `value` is not same as `label` in Select options.');
167 }
168 }
169 }
170 return {
171 label: rawLabel,
172 value: rawValue,
173 key: rawKey,
174 disabled: rawDisabled,
175 title: rawTitle
176 };
177 });
178 }, [mergedFieldNames, optionLabelProp, valueOptions]);
179
180 // =========================== Values ===========================
181 var _useMergedState3 = useMergedState(defaultValue, {
182 value: value
183 }),
184 _useMergedState4 = _slicedToArray(_useMergedState3, 2),
185 internalValue = _useMergedState4[0],
186 setInternalValue = _useMergedState4[1];
187
188 // Merged value with LabelValueType
189 var rawLabeledValues = React.useMemo(function () {
190 var _values$;
191 var values = convert2LabelValues(internalValue);
192
193 // combobox no need save value when it's no value (exclude value equal 0)
194 if (mode === 'combobox' && isComboNoValue((_values$ = values[0]) === null || _values$ === void 0 ? void 0 : _values$.value)) {
195 return [];
196 }
197 return values;
198 }, [internalValue, convert2LabelValues, mode]);
199
200 // Fill label with cache to avoid option remove
201 var _useCache = useCache(rawLabeledValues, valueOptions),
202 _useCache2 = _slicedToArray(_useCache, 2),
203 mergedValues = _useCache2[0],
204 getMixedOption = _useCache2[1];
205 var displayValues = React.useMemo(function () {
206 // `null` need show as placeholder instead
207 // https://github.com/ant-design/ant-design/issues/25057
208 if (!mode && mergedValues.length === 1) {
209 var firstValue = mergedValues[0];
210 if (firstValue.value === null && (firstValue.label === null || firstValue.label === undefined)) {
211 return [];
212 }
213 }
214 return mergedValues.map(function (item) {
215 var _item$label;
216 return _objectSpread(_objectSpread({}, item), {}, {
217 label: (_item$label = item.label) !== null && _item$label !== void 0 ? _item$label : item.value
218 });
219 });
220 }, [mode, mergedValues]);
221
222 /** Convert `displayValues` to raw value type set */
223 var rawValues = React.useMemo(function () {
224 return new Set(mergedValues.map(function (val) {
225 return val.value;
226 }));
227 }, [mergedValues]);
228 React.useEffect(function () {
229 if (mode === 'combobox') {
230 var _mergedValues$;
231 var strValue = (_mergedValues$ = mergedValues[0]) === null || _mergedValues$ === void 0 ? void 0 : _mergedValues$.value;
232 setSearchValue(hasValue(strValue) ? String(strValue) : '');
233 }
234 }, [mergedValues]);
235
236 // ======================= Display Option =======================
237 // Create a placeholder item if not exist in `options`
238 var createTagOption = useRefFunc(function (val, label) {
239 var _ref;
240 var mergedLabel = label !== null && label !== void 0 ? label : val;
241 return _ref = {}, _defineProperty(_ref, mergedFieldNames.value, val), _defineProperty(_ref, mergedFieldNames.label, mergedLabel), _ref;
242 });
243
244 // Fill tag as option if mode is `tags`
245 var filledTagOptions = React.useMemo(function () {
246 if (mode !== 'tags') {
247 return mergedOptions;
248 }
249
250 // >>> Tag mode
251 var cloneOptions = _toConsumableArray(mergedOptions);
252
253 // Check if value exist in options (include new patch item)
254 var existOptions = function existOptions(val) {
255 return valueOptions.has(val);
256 };
257
258 // Fill current value as option
259 _toConsumableArray(mergedValues).sort(function (a, b) {
260 return a.value < b.value ? -1 : 1;
261 }).forEach(function (item) {
262 var val = item.value;
263 if (!existOptions(val)) {
264 cloneOptions.push(createTagOption(val, item.label));
265 }
266 });
267 return cloneOptions;
268 }, [createTagOption, mergedOptions, valueOptions, mergedValues, mode]);
269 var filteredOptions = useFilterOptions(filledTagOptions, mergedFieldNames, mergedSearchValue, mergedFilterOption, optionFilterProp);
270
271 // Fill options with search value if needed
272 var filledSearchOptions = React.useMemo(function () {
273 if (mode !== 'tags' || !mergedSearchValue || filteredOptions.some(function (item) {
274 return item[optionFilterProp || 'value'] === mergedSearchValue;
275 })) {
276 return filteredOptions;
277 }
278
279 // Fill search value as option
280 return [createTagOption(mergedSearchValue)].concat(_toConsumableArray(filteredOptions));
281 }, [createTagOption, optionFilterProp, mode, filteredOptions, mergedSearchValue]);
282 var orderedFilteredOptions = React.useMemo(function () {
283 if (!filterSort) {
284 return filledSearchOptions;
285 }
286 return _toConsumableArray(filledSearchOptions).sort(function (a, b) {
287 return filterSort(a, b);
288 });
289 }, [filledSearchOptions, filterSort]);
290 var displayOptions = React.useMemo(function () {
291 return flattenOptions(orderedFilteredOptions, {
292 fieldNames: mergedFieldNames,
293 childrenAsData: childrenAsData
294 });
295 }, [orderedFilteredOptions, mergedFieldNames, childrenAsData]);
296
297 // =========================== Change ===========================
298 var triggerChange = function triggerChange(values) {
299 var labeledValues = convert2LabelValues(values);
300 setInternalValue(labeledValues);
301 if (onChange && (
302 // Trigger event only when value changed
303 labeledValues.length !== mergedValues.length || labeledValues.some(function (newVal, index) {
304 var _mergedValues$index;
305 return ((_mergedValues$index = mergedValues[index]) === null || _mergedValues$index === void 0 ? void 0 : _mergedValues$index.value) !== (newVal === null || newVal === void 0 ? void 0 : newVal.value);
306 }))) {
307 var returnValues = labelInValue ? labeledValues : labeledValues.map(function (v) {
308 return v.value;
309 });
310 var returnOptions = labeledValues.map(function (v) {
311 return injectPropsWithOption(getMixedOption(v.value));
312 });
313 onChange(
314 // Value
315 multiple ? returnValues : returnValues[0],
316 // Option
317 multiple ? returnOptions : returnOptions[0]);
318 }
319 };
320
321 // ======================= Accessibility ========================
322 var _React$useState = React.useState(null),
323 _React$useState2 = _slicedToArray(_React$useState, 2),
324 activeValue = _React$useState2[0],
325 setActiveValue = _React$useState2[1];
326 var _React$useState3 = React.useState(0),
327 _React$useState4 = _slicedToArray(_React$useState3, 2),
328 accessibilityIndex = _React$useState4[0],
329 setAccessibilityIndex = _React$useState4[1];
330 var mergedDefaultActiveFirstOption = defaultActiveFirstOption !== undefined ? defaultActiveFirstOption : mode !== 'combobox';
331 var onActiveValue = React.useCallback(function (active, index) {
332 var _ref2 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {},
333 _ref2$source = _ref2.source,
334 source = _ref2$source === void 0 ? 'keyboard' : _ref2$source;
335 setAccessibilityIndex(index);
336 if (backfill && mode === 'combobox' && active !== null && source === 'keyboard') {
337 setActiveValue(String(active));
338 }
339 }, [backfill, mode]);
340
341 // ========================= OptionList =========================
342 var triggerSelect = function triggerSelect(val, selected, type) {
343 var getSelectEnt = function getSelectEnt() {
344 var _option$key2;
345 var option = getMixedOption(val);
346 return [labelInValue ? {
347 label: option === null || option === void 0 ? void 0 : option[mergedFieldNames.label],
348 value: val,
349 key: (_option$key2 = option === null || option === void 0 ? void 0 : option.key) !== null && _option$key2 !== void 0 ? _option$key2 : val
350 } : val, injectPropsWithOption(option)];
351 };
352 if (selected && onSelect) {
353 var _getSelectEnt = getSelectEnt(),
354 _getSelectEnt2 = _slicedToArray(_getSelectEnt, 2),
355 wrappedValue = _getSelectEnt2[0],
356 _option = _getSelectEnt2[1];
357 onSelect(wrappedValue, _option);
358 } else if (!selected && onDeselect && type !== 'clear') {
359 var _getSelectEnt3 = getSelectEnt(),
360 _getSelectEnt4 = _slicedToArray(_getSelectEnt3, 2),
361 _wrappedValue = _getSelectEnt4[0],
362 _option2 = _getSelectEnt4[1];
363 onDeselect(_wrappedValue, _option2);
364 }
365 };
366
367 // Used for OptionList selection
368 var onInternalSelect = useRefFunc(function (val, info) {
369 var cloneValues;
370
371 // Single mode always trigger select only with option list
372 var mergedSelect = multiple ? info.selected : true;
373 if (mergedSelect) {
374 cloneValues = multiple ? [].concat(_toConsumableArray(mergedValues), [val]) : [val];
375 } else {
376 cloneValues = mergedValues.filter(function (v) {
377 return v.value !== val;
378 });
379 }
380 triggerChange(cloneValues);
381 triggerSelect(val, mergedSelect);
382
383 // Clean search value if single or configured
384 if (mode === 'combobox') {
385 // setSearchValue(String(val));
386 setActiveValue('');
387 } else if (!isMultiple || autoClearSearchValue) {
388 setSearchValue('');
389 setActiveValue('');
390 }
391 });
392
393 // ======================= Display Change =======================
394 // BaseSelect display values change
395 var onDisplayValuesChange = function onDisplayValuesChange(nextValues, info) {
396 triggerChange(nextValues);
397 var type = info.type,
398 values = info.values;
399 if (type === 'remove' || type === 'clear') {
400 values.forEach(function (item) {
401 triggerSelect(item.value, false, type);
402 });
403 }
404 };
405
406 // =========================== Search ===========================
407 var onInternalSearch = function onInternalSearch(searchText, info) {
408 setSearchValue(searchText);
409 setActiveValue(null);
410
411 // [Submit] Tag mode should flush input
412 if (info.source === 'submit') {
413 var formatted = (searchText || '').trim();
414 // prevent empty tags from appearing when you click the Enter button
415 if (formatted) {
416 var newRawValues = Array.from(new Set([].concat(_toConsumableArray(rawValues), [formatted])));
417 triggerChange(newRawValues);
418 triggerSelect(formatted, true);
419 setSearchValue('');
420 }
421 return;
422 }
423 if (info.source !== 'blur') {
424 if (mode === 'combobox') {
425 triggerChange(searchText);
426 }
427 onSearch === null || onSearch === void 0 ? void 0 : onSearch(searchText);
428 }
429 };
430 var onInternalSearchSplit = function onInternalSearchSplit(words) {
431 var patchValues = words;
432 if (mode !== 'tags') {
433 patchValues = words.map(function (word) {
434 var opt = labelOptions.get(word);
435 return opt === null || opt === void 0 ? void 0 : opt.value;
436 }).filter(function (val) {
437 return val !== undefined;
438 });
439 }
440 var newRawValues = Array.from(new Set([].concat(_toConsumableArray(rawValues), _toConsumableArray(patchValues))));
441 triggerChange(newRawValues);
442 newRawValues.forEach(function (newRawValue) {
443 triggerSelect(newRawValue, true);
444 });
445 };
446
447 // ========================== Context ===========================
448 var selectContext = React.useMemo(function () {
449 var realVirtual = virtual !== false && dropdownMatchSelectWidth !== false;
450 return _objectSpread(_objectSpread({}, parsedOptions), {}, {
451 flattenOptions: displayOptions,
452 onActiveValue: onActiveValue,
453 defaultActiveFirstOption: mergedDefaultActiveFirstOption,
454 onSelect: onInternalSelect,
455 menuItemSelectedIcon: menuItemSelectedIcon,
456 rawValues: rawValues,
457 fieldNames: mergedFieldNames,
458 virtual: realVirtual,
459 direction: direction,
460 listHeight: listHeight,
461 listItemHeight: listItemHeight,
462 childrenAsData: childrenAsData
463 });
464 }, [parsedOptions, displayOptions, onActiveValue, mergedDefaultActiveFirstOption, onInternalSelect, menuItemSelectedIcon, rawValues, mergedFieldNames, virtual, dropdownMatchSelectWidth, listHeight, listItemHeight, childrenAsData]);
465
466 // ========================== Warning ===========================
467 if (process.env.NODE_ENV !== 'production') {
468 warningProps(props);
469 warningNullOptions(mergedOptions, mergedFieldNames);
470 }
471
472 // ==============================================================
473 // == Render ==
474 // ==============================================================
475 return /*#__PURE__*/React.createElement(SelectContext.Provider, {
476 value: selectContext
477 }, /*#__PURE__*/React.createElement(BaseSelect, _extends({}, restProps, {
478 // >>> MISC
479 id: mergedId,
480 prefixCls: prefixCls,
481 ref: ref,
482 omitDomProps: OMIT_DOM_PROPS,
483 mode: mode
484 // >>> Values
485 ,
486 displayValues: displayValues,
487 onDisplayValuesChange: onDisplayValuesChange
488 // >>> Trigger
489 ,
490 direction: direction
491 // >>> Search
492 ,
493 searchValue: mergedSearchValue,
494 onSearch: onInternalSearch,
495 autoClearSearchValue: autoClearSearchValue,
496 onSearchSplit: onInternalSearchSplit,
497 dropdownMatchSelectWidth: dropdownMatchSelectWidth
498 // >>> OptionList
499 ,
500 OptionList: OptionList,
501 emptyOptions: !displayOptions.length
502 // >>> Accessibility
503 ,
504 activeValue: activeValue,
505 activeDescendantId: "".concat(mergedId, "_list_").concat(accessibilityIndex)
506 })));
507});
508if (process.env.NODE_ENV !== 'production') {
509 Select.displayName = 'Select';
510}
511var TypedSelect = Select;
512TypedSelect.Option = Option;
513TypedSelect.OptGroup = OptGroup;
514export default TypedSelect;
\No newline at end of file