OptionList.js 14 KB

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