UNPKG

13.2 kBJavaScriptView Raw
1import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
2import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
3import _extends from "@babel/runtime/helpers/esm/extends";
4import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray";
5import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
6var _excluded = ["disabled", "title", "children", "style", "className"];
7import classNames from 'classnames';
8import useMemo from "rc-util/es/hooks/useMemo";
9import KeyCode from "rc-util/es/KeyCode";
10import omit from "rc-util/es/omit";
11import pickAttrs from "rc-util/es/pickAttrs";
12import List from 'rc-virtual-list';
13import * as React from 'react';
14import { useEffect } from 'react';
15import useBaseProps from "./hooks/useBaseProps";
16import SelectContext from "./SelectContext";
17import TransBtn from "./TransBtn";
18import { isPlatformMac } from "./utils/platformUtil";
19
20// export interface OptionListProps<OptionsType extends object[]> {
21
22function isTitleType(content) {
23 return typeof content === 'string' || typeof content === 'number';
24}
25
26/**
27 * Using virtual list of option display.
28 * Will fallback to dom if use customize render.
29 */
30var OptionList = function OptionList(_, ref) {
31 var _useBaseProps = useBaseProps(),
32 prefixCls = _useBaseProps.prefixCls,
33 id = _useBaseProps.id,
34 open = _useBaseProps.open,
35 multiple = _useBaseProps.multiple,
36 mode = _useBaseProps.mode,
37 searchValue = _useBaseProps.searchValue,
38 toggleOpen = _useBaseProps.toggleOpen,
39 notFoundContent = _useBaseProps.notFoundContent,
40 onPopupScroll = _useBaseProps.onPopupScroll;
41 var _React$useContext = React.useContext(SelectContext),
42 flattenOptions = _React$useContext.flattenOptions,
43 onActiveValue = _React$useContext.onActiveValue,
44 defaultActiveFirstOption = _React$useContext.defaultActiveFirstOption,
45 onSelect = _React$useContext.onSelect,
46 menuItemSelectedIcon = _React$useContext.menuItemSelectedIcon,
47 rawValues = _React$useContext.rawValues,
48 fieldNames = _React$useContext.fieldNames,
49 virtual = _React$useContext.virtual,
50 direction = _React$useContext.direction,
51 listHeight = _React$useContext.listHeight,
52 listItemHeight = _React$useContext.listItemHeight;
53 var itemPrefixCls = "".concat(prefixCls, "-item");
54 var memoFlattenOptions = useMemo(function () {
55 return flattenOptions;
56 }, [open, flattenOptions], function (prev, next) {
57 return next[0] && prev[1] !== next[1];
58 });
59
60 // =========================== List ===========================
61 var listRef = React.useRef(null);
62 var onListMouseDown = function onListMouseDown(event) {
63 event.preventDefault();
64 };
65 var scrollIntoView = function scrollIntoView(args) {
66 if (listRef.current) {
67 listRef.current.scrollTo(typeof args === 'number' ? {
68 index: args
69 } : args);
70 }
71 };
72
73 // ========================== Active ==========================
74 var getEnabledActiveIndex = function getEnabledActiveIndex(index) {
75 var offset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
76 var len = memoFlattenOptions.length;
77 for (var i = 0; i < len; i += 1) {
78 var current = (index + i * offset + len) % len;
79 var _memoFlattenOptions$c = memoFlattenOptions[current],
80 group = _memoFlattenOptions$c.group,
81 data = _memoFlattenOptions$c.data;
82 if (!group && !data.disabled) {
83 return current;
84 }
85 }
86 return -1;
87 };
88 var _React$useState = React.useState(function () {
89 return getEnabledActiveIndex(0);
90 }),
91 _React$useState2 = _slicedToArray(_React$useState, 2),
92 activeIndex = _React$useState2[0],
93 setActiveIndex = _React$useState2[1];
94 var setActive = function setActive(index) {
95 var fromKeyboard = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
96 setActiveIndex(index);
97 var info = {
98 source: fromKeyboard ? 'keyboard' : 'mouse'
99 };
100
101 // Trigger active event
102 var flattenItem = memoFlattenOptions[index];
103 if (!flattenItem) {
104 onActiveValue(null, -1, info);
105 return;
106 }
107 onActiveValue(flattenItem.value, index, info);
108 };
109
110 // Auto active first item when list length or searchValue changed
111 useEffect(function () {
112 setActive(defaultActiveFirstOption !== false ? getEnabledActiveIndex(0) : -1);
113 }, [memoFlattenOptions.length, searchValue]);
114
115 // https://github.com/ant-design/ant-design/issues/34975
116 var isSelected = React.useCallback(function (value) {
117 return rawValues.has(value) && mode !== 'combobox';
118 }, [mode, _toConsumableArray(rawValues).toString(), rawValues.size]);
119
120 // Auto scroll to item position in single mode
121 useEffect(function () {
122 /**
123 * React will skip `onChange` when component update.
124 * `setActive` function will call root accessibility state update which makes re-render.
125 * So we need to delay to let Input component trigger onChange first.
126 */
127 var timeoutId = setTimeout(function () {
128 if (!multiple && open && rawValues.size === 1) {
129 var value = Array.from(rawValues)[0];
130 var index = memoFlattenOptions.findIndex(function (_ref) {
131 var data = _ref.data;
132 return data.value === value;
133 });
134 if (index !== -1) {
135 setActive(index);
136 scrollIntoView(index);
137 }
138 }
139 });
140
141 // Force trigger scrollbar visible when open
142 if (open) {
143 var _listRef$current;
144 (_listRef$current = listRef.current) === null || _listRef$current === void 0 ? void 0 : _listRef$current.scrollTo(undefined);
145 }
146 return function () {
147 return clearTimeout(timeoutId);
148 };
149 }, [open, searchValue, flattenOptions.length]);
150
151 // ========================== Values ==========================
152 var onSelectValue = function onSelectValue(value) {
153 if (value !== undefined) {
154 onSelect(value, {
155 selected: !rawValues.has(value)
156 });
157 }
158
159 // Single mode should always close by select
160 if (!multiple) {
161 toggleOpen(false);
162 }
163 };
164
165 // ========================= Keyboard =========================
166 React.useImperativeHandle(ref, function () {
167 return {
168 onKeyDown: function onKeyDown(event) {
169 var which = event.which,
170 ctrlKey = event.ctrlKey;
171 switch (which) {
172 // >>> Arrow keys & ctrl + n/p on Mac
173 case KeyCode.N:
174 case KeyCode.P:
175 case KeyCode.UP:
176 case KeyCode.DOWN:
177 {
178 var offset = 0;
179 if (which === KeyCode.UP) {
180 offset = -1;
181 } else if (which === KeyCode.DOWN) {
182 offset = 1;
183 } else if (isPlatformMac() && ctrlKey) {
184 if (which === KeyCode.N) {
185 offset = 1;
186 } else if (which === KeyCode.P) {
187 offset = -1;
188 }
189 }
190 if (offset !== 0) {
191 var nextActiveIndex = getEnabledActiveIndex(activeIndex + offset, offset);
192 scrollIntoView(nextActiveIndex);
193 setActive(nextActiveIndex, true);
194 }
195 break;
196 }
197
198 // >>> Select
199 case KeyCode.ENTER:
200 {
201 // value
202 var item = memoFlattenOptions[activeIndex];
203 if (item && !item.data.disabled) {
204 onSelectValue(item.value);
205 } else {
206 onSelectValue(undefined);
207 }
208 if (open) {
209 event.preventDefault();
210 }
211 break;
212 }
213
214 // >>> Close
215 case KeyCode.ESC:
216 {
217 toggleOpen(false);
218 if (open) {
219 event.stopPropagation();
220 }
221 }
222 }
223 },
224 onKeyUp: function onKeyUp() {},
225 scrollTo: function scrollTo(index) {
226 scrollIntoView(index);
227 }
228 };
229 });
230
231 // ========================== Render ==========================
232 if (memoFlattenOptions.length === 0) {
233 return /*#__PURE__*/React.createElement("div", {
234 role: "listbox",
235 id: "".concat(id, "_list"),
236 className: "".concat(itemPrefixCls, "-empty"),
237 onMouseDown: onListMouseDown
238 }, notFoundContent);
239 }
240 var omitFieldNameList = Object.keys(fieldNames).map(function (key) {
241 return fieldNames[key];
242 });
243 var getLabel = function getLabel(item) {
244 return item.label;
245 };
246 function getItemAriaProps(item, index) {
247 var group = item.group;
248 return {
249 role: group ? 'presentation' : 'option',
250 id: "".concat(id, "_list_").concat(index)
251 };
252 }
253 var renderItem = function renderItem(index) {
254 var item = memoFlattenOptions[index];
255 if (!item) return null;
256 var itemData = item.data || {};
257 var value = itemData.value;
258 var group = item.group;
259 var attrs = pickAttrs(itemData, true);
260 var mergedLabel = getLabel(item);
261 return item ? /*#__PURE__*/React.createElement("div", _extends({
262 "aria-label": typeof mergedLabel === 'string' && !group ? mergedLabel : null
263 }, attrs, {
264 key: index
265 }, getItemAriaProps(item, index), {
266 "aria-selected": isSelected(value)
267 }), value) : null;
268 };
269 var a11yProps = {
270 role: 'listbox',
271 id: "".concat(id, "_list")
272 };
273 return /*#__PURE__*/React.createElement(React.Fragment, null, virtual && /*#__PURE__*/React.createElement("div", _extends({}, a11yProps, {
274 style: {
275 height: 0,
276 width: 0,
277 overflow: 'hidden'
278 }
279 }), renderItem(activeIndex - 1), renderItem(activeIndex), renderItem(activeIndex + 1)), /*#__PURE__*/React.createElement(List, {
280 itemKey: "key",
281 ref: listRef,
282 data: memoFlattenOptions,
283 height: listHeight,
284 itemHeight: listItemHeight,
285 fullHeight: false,
286 onMouseDown: onListMouseDown,
287 onScroll: onPopupScroll,
288 virtual: virtual,
289 direction: direction,
290 innerProps: virtual ? null : a11yProps
291 }, function (item, itemIndex) {
292 var _classNames;
293 var group = item.group,
294 groupOption = item.groupOption,
295 data = item.data,
296 label = item.label,
297 value = item.value;
298 var key = data.key;
299
300 // Group
301 if (group) {
302 var _data$title;
303 var groupTitle = (_data$title = data.title) !== null && _data$title !== void 0 ? _data$title : isTitleType(label) ? label.toString() : undefined;
304 return /*#__PURE__*/React.createElement("div", {
305 className: classNames(itemPrefixCls, "".concat(itemPrefixCls, "-group")),
306 title: groupTitle
307 }, label !== undefined ? label : key);
308 }
309 var disabled = data.disabled,
310 title = data.title,
311 children = data.children,
312 style = data.style,
313 className = data.className,
314 otherProps = _objectWithoutProperties(data, _excluded);
315 var passedProps = omit(otherProps, omitFieldNameList);
316
317 // Option
318 var selected = isSelected(value);
319 var optionPrefixCls = "".concat(itemPrefixCls, "-option");
320 var optionClassName = classNames(itemPrefixCls, optionPrefixCls, className, (_classNames = {}, _defineProperty(_classNames, "".concat(optionPrefixCls, "-grouped"), groupOption), _defineProperty(_classNames, "".concat(optionPrefixCls, "-active"), activeIndex === itemIndex && !disabled), _defineProperty(_classNames, "".concat(optionPrefixCls, "-disabled"), disabled), _defineProperty(_classNames, "".concat(optionPrefixCls, "-selected"), selected), _classNames));
321 var mergedLabel = getLabel(item);
322 var iconVisible = !menuItemSelectedIcon || typeof menuItemSelectedIcon === 'function' || selected;
323
324 // https://github.com/ant-design/ant-design/issues/34145
325 var content = typeof mergedLabel === 'number' ? mergedLabel : mergedLabel || value;
326 // https://github.com/ant-design/ant-design/issues/26717
327 var optionTitle = isTitleType(content) ? content.toString() : undefined;
328 if (title !== undefined) {
329 optionTitle = title;
330 }
331 return /*#__PURE__*/React.createElement("div", _extends({}, pickAttrs(passedProps), !virtual ? getItemAriaProps(item, itemIndex) : {}, {
332 "aria-selected": selected,
333 className: optionClassName,
334 title: optionTitle,
335 onMouseMove: function onMouseMove() {
336 if (activeIndex === itemIndex || disabled) {
337 return;
338 }
339 setActive(itemIndex);
340 },
341 onClick: function onClick() {
342 if (!disabled) {
343 onSelectValue(value);
344 }
345 },
346 style: style
347 }), /*#__PURE__*/React.createElement("div", {
348 className: "".concat(optionPrefixCls, "-content")
349 }, content), /*#__PURE__*/React.isValidElement(menuItemSelectedIcon) || selected, iconVisible && /*#__PURE__*/React.createElement(TransBtn, {
350 className: "".concat(itemPrefixCls, "-option-state"),
351 customizeIcon: menuItemSelectedIcon,
352 customizeIconProps: {
353 isSelected: selected
354 }
355 }, selected ? '✓' : null));
356 }));
357};
358var RefOptionList = /*#__PURE__*/React.forwardRef(OptionList);
359RefOptionList.displayName = 'OptionList';
360export default RefOptionList;
\No newline at end of file