List.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. import _extends from "@babel/runtime/helpers/esm/extends";
  2. import _typeof from "@babel/runtime/helpers/esm/typeof";
  3. import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
  4. import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
  5. import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
  6. import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
  7. var _excluded = ["prefixCls", "className", "height", "itemHeight", "fullHeight", "style", "data", "children", "itemKey", "virtual", "direction", "scrollWidth", "component", "onScroll", "onVirtualScroll", "onVisibleChange", "innerProps", "extraRender", "styles", "showScrollBar"];
  8. import classNames from 'classnames';
  9. import ResizeObserver from 'rc-resize-observer';
  10. import { useEvent } from 'rc-util';
  11. import useLayoutEffect from "rc-util/es/hooks/useLayoutEffect";
  12. import * as React from 'react';
  13. import { useRef, useState } from 'react';
  14. import { flushSync } from 'react-dom';
  15. import Filler from "./Filler";
  16. import useChildren from "./hooks/useChildren";
  17. import useDiffItem from "./hooks/useDiffItem";
  18. import useFrameWheel from "./hooks/useFrameWheel";
  19. import { useGetSize } from "./hooks/useGetSize";
  20. import useHeights from "./hooks/useHeights";
  21. import useMobileTouchMove from "./hooks/useMobileTouchMove";
  22. import useOriginScroll from "./hooks/useOriginScroll";
  23. import useScrollDrag from "./hooks/useScrollDrag";
  24. import useScrollTo from "./hooks/useScrollTo";
  25. import ScrollBar from "./ScrollBar";
  26. import { getSpinSize } from "./utils/scrollbarUtil";
  27. var EMPTY_DATA = [];
  28. var ScrollStyle = {
  29. overflowY: 'auto',
  30. overflowAnchor: 'none'
  31. };
  32. export function RawList(props, ref) {
  33. var _props$prefixCls = props.prefixCls,
  34. prefixCls = _props$prefixCls === void 0 ? 'rc-virtual-list' : _props$prefixCls,
  35. className = props.className,
  36. height = props.height,
  37. itemHeight = props.itemHeight,
  38. _props$fullHeight = props.fullHeight,
  39. fullHeight = _props$fullHeight === void 0 ? true : _props$fullHeight,
  40. style = props.style,
  41. data = props.data,
  42. children = props.children,
  43. itemKey = props.itemKey,
  44. virtual = props.virtual,
  45. direction = props.direction,
  46. scrollWidth = props.scrollWidth,
  47. _props$component = props.component,
  48. Component = _props$component === void 0 ? 'div' : _props$component,
  49. onScroll = props.onScroll,
  50. onVirtualScroll = props.onVirtualScroll,
  51. onVisibleChange = props.onVisibleChange,
  52. innerProps = props.innerProps,
  53. extraRender = props.extraRender,
  54. styles = props.styles,
  55. _props$showScrollBar = props.showScrollBar,
  56. showScrollBar = _props$showScrollBar === void 0 ? 'optional' : _props$showScrollBar,
  57. restProps = _objectWithoutProperties(props, _excluded);
  58. // =============================== Item Key ===============================
  59. var getKey = React.useCallback(function (item) {
  60. if (typeof itemKey === 'function') {
  61. return itemKey(item);
  62. }
  63. return item === null || item === void 0 ? void 0 : item[itemKey];
  64. }, [itemKey]);
  65. // ================================ Height ================================
  66. var _useHeights = useHeights(getKey, null, null),
  67. _useHeights2 = _slicedToArray(_useHeights, 4),
  68. setInstanceRef = _useHeights2[0],
  69. collectHeight = _useHeights2[1],
  70. heights = _useHeights2[2],
  71. heightUpdatedMark = _useHeights2[3];
  72. // ================================= MISC =================================
  73. var useVirtual = !!(virtual !== false && height && itemHeight);
  74. var containerHeight = React.useMemo(function () {
  75. return Object.values(heights.maps).reduce(function (total, curr) {
  76. return total + curr;
  77. }, 0);
  78. }, [heights.id, heights.maps]);
  79. var inVirtual = useVirtual && data && (Math.max(itemHeight * data.length, containerHeight) > height || !!scrollWidth);
  80. var isRTL = direction === 'rtl';
  81. var mergedClassName = classNames(prefixCls, _defineProperty({}, "".concat(prefixCls, "-rtl"), isRTL), className);
  82. var mergedData = data || EMPTY_DATA;
  83. var componentRef = useRef();
  84. var fillerInnerRef = useRef();
  85. var containerRef = useRef();
  86. // =============================== Item Key ===============================
  87. var _useState = useState(0),
  88. _useState2 = _slicedToArray(_useState, 2),
  89. offsetTop = _useState2[0],
  90. setOffsetTop = _useState2[1];
  91. var _useState3 = useState(0),
  92. _useState4 = _slicedToArray(_useState3, 2),
  93. offsetLeft = _useState4[0],
  94. setOffsetLeft = _useState4[1];
  95. var _useState5 = useState(false),
  96. _useState6 = _slicedToArray(_useState5, 2),
  97. scrollMoving = _useState6[0],
  98. setScrollMoving = _useState6[1];
  99. var onScrollbarStartMove = function onScrollbarStartMove() {
  100. setScrollMoving(true);
  101. };
  102. var onScrollbarStopMove = function onScrollbarStopMove() {
  103. setScrollMoving(false);
  104. };
  105. var sharedConfig = {
  106. getKey: getKey
  107. };
  108. // ================================ Scroll ================================
  109. function syncScrollTop(newTop) {
  110. setOffsetTop(function (origin) {
  111. var value;
  112. if (typeof newTop === 'function') {
  113. value = newTop(origin);
  114. } else {
  115. value = newTop;
  116. }
  117. var alignedTop = keepInRange(value);
  118. componentRef.current.scrollTop = alignedTop;
  119. return alignedTop;
  120. });
  121. }
  122. // ================================ Legacy ================================
  123. // Put ref here since the range is generate by follow
  124. var rangeRef = useRef({
  125. start: 0,
  126. end: mergedData.length
  127. });
  128. var diffItemRef = useRef();
  129. var _useDiffItem = useDiffItem(mergedData, getKey),
  130. _useDiffItem2 = _slicedToArray(_useDiffItem, 1),
  131. diffItem = _useDiffItem2[0];
  132. diffItemRef.current = diffItem;
  133. // ========================== Visible Calculation =========================
  134. var _React$useMemo = React.useMemo(function () {
  135. if (!useVirtual) {
  136. return {
  137. scrollHeight: undefined,
  138. start: 0,
  139. end: mergedData.length - 1,
  140. offset: undefined
  141. };
  142. }
  143. // Always use virtual scroll bar in avoid shaking
  144. if (!inVirtual) {
  145. var _fillerInnerRef$curre;
  146. return {
  147. scrollHeight: ((_fillerInnerRef$curre = fillerInnerRef.current) === null || _fillerInnerRef$curre === void 0 ? void 0 : _fillerInnerRef$curre.offsetHeight) || 0,
  148. start: 0,
  149. end: mergedData.length - 1,
  150. offset: undefined
  151. };
  152. }
  153. var itemTop = 0;
  154. var startIndex;
  155. var startOffset;
  156. var endIndex;
  157. var dataLen = mergedData.length;
  158. for (var i = 0; i < dataLen; i += 1) {
  159. var _item = mergedData[i];
  160. var key = getKey(_item);
  161. var cacheHeight = heights.get(key);
  162. var currentItemBottom = itemTop + (cacheHeight === undefined ? itemHeight : cacheHeight);
  163. // Check item top in the range
  164. if (currentItemBottom >= offsetTop && startIndex === undefined) {
  165. startIndex = i;
  166. startOffset = itemTop;
  167. }
  168. // Check item bottom in the range. We will render additional one item for motion usage
  169. if (currentItemBottom > offsetTop + height && endIndex === undefined) {
  170. endIndex = i;
  171. }
  172. itemTop = currentItemBottom;
  173. }
  174. // When scrollTop at the end but data cut to small count will reach this
  175. if (startIndex === undefined) {
  176. startIndex = 0;
  177. startOffset = 0;
  178. endIndex = Math.ceil(height / itemHeight);
  179. }
  180. if (endIndex === undefined) {
  181. endIndex = mergedData.length - 1;
  182. }
  183. // Give cache to improve scroll experience
  184. endIndex = Math.min(endIndex + 1, mergedData.length - 1);
  185. return {
  186. scrollHeight: itemTop,
  187. start: startIndex,
  188. end: endIndex,
  189. offset: startOffset
  190. };
  191. }, [inVirtual, useVirtual, offsetTop, mergedData, heightUpdatedMark, height]),
  192. scrollHeight = _React$useMemo.scrollHeight,
  193. start = _React$useMemo.start,
  194. end = _React$useMemo.end,
  195. fillerOffset = _React$useMemo.offset;
  196. rangeRef.current.start = start;
  197. rangeRef.current.end = end;
  198. // When scroll up, first visible item get real height may not same as `itemHeight`,
  199. // Which will make scroll jump.
  200. // Let's sync scroll top to avoid jump
  201. React.useLayoutEffect(function () {
  202. var changedRecord = heights.getRecord();
  203. if (changedRecord.size === 1) {
  204. var recordKey = Array.from(changedRecord.keys())[0];
  205. var prevCacheHeight = changedRecord.get(recordKey);
  206. // Quick switch data may cause `start` not in `mergedData` anymore
  207. var startItem = mergedData[start];
  208. if (startItem && prevCacheHeight === undefined) {
  209. var startIndexKey = getKey(startItem);
  210. if (startIndexKey === recordKey) {
  211. var realStartHeight = heights.get(recordKey);
  212. var diffHeight = realStartHeight - itemHeight;
  213. syncScrollTop(function (ori) {
  214. return ori + diffHeight;
  215. });
  216. }
  217. }
  218. }
  219. heights.resetRecord();
  220. }, [scrollHeight]);
  221. // ================================= Size =================================
  222. var _React$useState = React.useState({
  223. width: 0,
  224. height: height
  225. }),
  226. _React$useState2 = _slicedToArray(_React$useState, 2),
  227. size = _React$useState2[0],
  228. setSize = _React$useState2[1];
  229. var onHolderResize = function onHolderResize(sizeInfo) {
  230. setSize({
  231. width: sizeInfo.offsetWidth,
  232. height: sizeInfo.offsetHeight
  233. });
  234. };
  235. // Hack on scrollbar to enable flash call
  236. var verticalScrollBarRef = useRef();
  237. var horizontalScrollBarRef = useRef();
  238. var horizontalScrollBarSpinSize = React.useMemo(function () {
  239. return getSpinSize(size.width, scrollWidth);
  240. }, [size.width, scrollWidth]);
  241. var verticalScrollBarSpinSize = React.useMemo(function () {
  242. return getSpinSize(size.height, scrollHeight);
  243. }, [size.height, scrollHeight]);
  244. // =============================== In Range ===============================
  245. var maxScrollHeight = scrollHeight - height;
  246. var maxScrollHeightRef = useRef(maxScrollHeight);
  247. maxScrollHeightRef.current = maxScrollHeight;
  248. function keepInRange(newScrollTop) {
  249. var newTop = newScrollTop;
  250. if (!Number.isNaN(maxScrollHeightRef.current)) {
  251. newTop = Math.min(newTop, maxScrollHeightRef.current);
  252. }
  253. newTop = Math.max(newTop, 0);
  254. return newTop;
  255. }
  256. var isScrollAtTop = offsetTop <= 0;
  257. var isScrollAtBottom = offsetTop >= maxScrollHeight;
  258. var isScrollAtLeft = offsetLeft <= 0;
  259. var isScrollAtRight = offsetLeft >= scrollWidth;
  260. var originScroll = useOriginScroll(isScrollAtTop, isScrollAtBottom, isScrollAtLeft, isScrollAtRight);
  261. // ================================ Scroll ================================
  262. var getVirtualScrollInfo = function getVirtualScrollInfo() {
  263. return {
  264. x: isRTL ? -offsetLeft : offsetLeft,
  265. y: offsetTop
  266. };
  267. };
  268. var lastVirtualScrollInfoRef = useRef(getVirtualScrollInfo());
  269. var triggerScroll = useEvent(function (params) {
  270. if (onVirtualScroll) {
  271. var nextInfo = _objectSpread(_objectSpread({}, getVirtualScrollInfo()), params);
  272. // Trigger when offset changed
  273. if (lastVirtualScrollInfoRef.current.x !== nextInfo.x || lastVirtualScrollInfoRef.current.y !== nextInfo.y) {
  274. onVirtualScroll(nextInfo);
  275. lastVirtualScrollInfoRef.current = nextInfo;
  276. }
  277. }
  278. });
  279. function onScrollBar(newScrollOffset, horizontal) {
  280. var newOffset = newScrollOffset;
  281. if (horizontal) {
  282. flushSync(function () {
  283. setOffsetLeft(newOffset);
  284. });
  285. triggerScroll();
  286. } else {
  287. syncScrollTop(newOffset);
  288. }
  289. }
  290. // When data size reduce. It may trigger native scroll event back to fit scroll position
  291. function onFallbackScroll(e) {
  292. var newScrollTop = e.currentTarget.scrollTop;
  293. if (newScrollTop !== offsetTop) {
  294. syncScrollTop(newScrollTop);
  295. }
  296. // Trigger origin onScroll
  297. onScroll === null || onScroll === void 0 || onScroll(e);
  298. triggerScroll();
  299. }
  300. var keepInHorizontalRange = function keepInHorizontalRange(nextOffsetLeft) {
  301. var tmpOffsetLeft = nextOffsetLeft;
  302. var max = !!scrollWidth ? scrollWidth - size.width : 0;
  303. tmpOffsetLeft = Math.max(tmpOffsetLeft, 0);
  304. tmpOffsetLeft = Math.min(tmpOffsetLeft, max);
  305. return tmpOffsetLeft;
  306. };
  307. var onWheelDelta = useEvent(function (offsetXY, fromHorizontal) {
  308. if (fromHorizontal) {
  309. flushSync(function () {
  310. setOffsetLeft(function (left) {
  311. var nextOffsetLeft = left + (isRTL ? -offsetXY : offsetXY);
  312. return keepInHorizontalRange(nextOffsetLeft);
  313. });
  314. });
  315. triggerScroll();
  316. } else {
  317. syncScrollTop(function (top) {
  318. var newTop = top + offsetXY;
  319. return newTop;
  320. });
  321. }
  322. });
  323. // Since this added in global,should use ref to keep update
  324. var _useFrameWheel = useFrameWheel(useVirtual, isScrollAtTop, isScrollAtBottom, isScrollAtLeft, isScrollAtRight, !!scrollWidth, onWheelDelta),
  325. _useFrameWheel2 = _slicedToArray(_useFrameWheel, 2),
  326. onRawWheel = _useFrameWheel2[0],
  327. onFireFoxScroll = _useFrameWheel2[1];
  328. // Mobile touch move
  329. useMobileTouchMove(useVirtual, componentRef, function (isHorizontal, delta, smoothOffset, e) {
  330. var event = e;
  331. if (originScroll(isHorizontal, delta, smoothOffset)) {
  332. return false;
  333. }
  334. // Fix nest List trigger TouchMove event
  335. if (!event || !event._virtualHandled) {
  336. if (event) {
  337. event._virtualHandled = true;
  338. }
  339. onRawWheel({
  340. preventDefault: function preventDefault() {},
  341. deltaX: isHorizontal ? delta : 0,
  342. deltaY: isHorizontal ? 0 : delta
  343. });
  344. return true;
  345. }
  346. return false;
  347. });
  348. // MouseDown drag for scroll
  349. useScrollDrag(inVirtual, componentRef, function (offset) {
  350. syncScrollTop(function (top) {
  351. return top + offset;
  352. });
  353. });
  354. useLayoutEffect(function () {
  355. // Firefox only
  356. function onMozMousePixelScroll(e) {
  357. // scrolling at top/bottom limit
  358. var scrollingUpAtTop = isScrollAtTop && e.detail < 0;
  359. var scrollingDownAtBottom = isScrollAtBottom && e.detail > 0;
  360. if (useVirtual && !scrollingUpAtTop && !scrollingDownAtBottom) {
  361. e.preventDefault();
  362. }
  363. }
  364. var componentEle = componentRef.current;
  365. componentEle.addEventListener('wheel', onRawWheel, {
  366. passive: false
  367. });
  368. componentEle.addEventListener('DOMMouseScroll', onFireFoxScroll, {
  369. passive: true
  370. });
  371. componentEle.addEventListener('MozMousePixelScroll', onMozMousePixelScroll, {
  372. passive: false
  373. });
  374. return function () {
  375. componentEle.removeEventListener('wheel', onRawWheel);
  376. componentEle.removeEventListener('DOMMouseScroll', onFireFoxScroll);
  377. componentEle.removeEventListener('MozMousePixelScroll', onMozMousePixelScroll);
  378. };
  379. }, [useVirtual, isScrollAtTop, isScrollAtBottom]);
  380. // Sync scroll left
  381. useLayoutEffect(function () {
  382. if (scrollWidth) {
  383. var newOffsetLeft = keepInHorizontalRange(offsetLeft);
  384. setOffsetLeft(newOffsetLeft);
  385. triggerScroll({
  386. x: newOffsetLeft
  387. });
  388. }
  389. }, [size.width, scrollWidth]);
  390. // ================================= Ref ==================================
  391. var delayHideScrollBar = function delayHideScrollBar() {
  392. var _verticalScrollBarRef, _horizontalScrollBarR;
  393. (_verticalScrollBarRef = verticalScrollBarRef.current) === null || _verticalScrollBarRef === void 0 || _verticalScrollBarRef.delayHidden();
  394. (_horizontalScrollBarR = horizontalScrollBarRef.current) === null || _horizontalScrollBarR === void 0 || _horizontalScrollBarR.delayHidden();
  395. };
  396. var _scrollTo = useScrollTo(componentRef, mergedData, heights, itemHeight, getKey, function () {
  397. return collectHeight(true);
  398. }, syncScrollTop, delayHideScrollBar);
  399. React.useImperativeHandle(ref, function () {
  400. return {
  401. nativeElement: containerRef.current,
  402. getScrollInfo: getVirtualScrollInfo,
  403. scrollTo: function scrollTo(config) {
  404. function isPosScroll(arg) {
  405. return arg && _typeof(arg) === 'object' && ('left' in arg || 'top' in arg);
  406. }
  407. if (isPosScroll(config)) {
  408. // Scroll X
  409. if (config.left !== undefined) {
  410. setOffsetLeft(keepInHorizontalRange(config.left));
  411. }
  412. // Scroll Y
  413. _scrollTo(config.top);
  414. } else {
  415. _scrollTo(config);
  416. }
  417. }
  418. };
  419. });
  420. // ================================ Effect ================================
  421. /** We need told outside that some list not rendered */
  422. useLayoutEffect(function () {
  423. if (onVisibleChange) {
  424. var renderList = mergedData.slice(start, end + 1);
  425. onVisibleChange(renderList, mergedData);
  426. }
  427. }, [start, end, mergedData]);
  428. // ================================ Extra =================================
  429. var getSize = useGetSize(mergedData, getKey, heights, itemHeight);
  430. var extraContent = extraRender === null || extraRender === void 0 ? void 0 : extraRender({
  431. start: start,
  432. end: end,
  433. virtual: inVirtual,
  434. offsetX: offsetLeft,
  435. offsetY: fillerOffset,
  436. rtl: isRTL,
  437. getSize: getSize
  438. });
  439. // ================================ Render ================================
  440. var listChildren = useChildren(mergedData, start, end, scrollWidth, offsetLeft, setInstanceRef, children, sharedConfig);
  441. var componentStyle = null;
  442. if (height) {
  443. componentStyle = _objectSpread(_defineProperty({}, fullHeight ? 'height' : 'maxHeight', height), ScrollStyle);
  444. if (useVirtual) {
  445. componentStyle.overflowY = 'hidden';
  446. if (scrollWidth) {
  447. componentStyle.overflowX = 'hidden';
  448. }
  449. if (scrollMoving) {
  450. componentStyle.pointerEvents = 'none';
  451. }
  452. }
  453. }
  454. var containerProps = {};
  455. if (isRTL) {
  456. containerProps.dir = 'rtl';
  457. }
  458. return /*#__PURE__*/React.createElement("div", _extends({
  459. ref: containerRef,
  460. style: _objectSpread(_objectSpread({}, style), {}, {
  461. position: 'relative'
  462. }),
  463. className: mergedClassName
  464. }, containerProps, restProps), /*#__PURE__*/React.createElement(ResizeObserver, {
  465. onResize: onHolderResize
  466. }, /*#__PURE__*/React.createElement(Component, {
  467. className: "".concat(prefixCls, "-holder"),
  468. style: componentStyle,
  469. ref: componentRef,
  470. onScroll: onFallbackScroll,
  471. onMouseEnter: delayHideScrollBar
  472. }, /*#__PURE__*/React.createElement(Filler, {
  473. prefixCls: prefixCls,
  474. height: scrollHeight,
  475. offsetX: offsetLeft,
  476. offsetY: fillerOffset,
  477. scrollWidth: scrollWidth,
  478. onInnerResize: collectHeight,
  479. ref: fillerInnerRef,
  480. innerProps: innerProps,
  481. rtl: isRTL,
  482. extra: extraContent
  483. }, listChildren))), inVirtual && scrollHeight > height && /*#__PURE__*/React.createElement(ScrollBar, {
  484. ref: verticalScrollBarRef,
  485. prefixCls: prefixCls,
  486. scrollOffset: offsetTop,
  487. scrollRange: scrollHeight,
  488. rtl: isRTL,
  489. onScroll: onScrollBar,
  490. onStartMove: onScrollbarStartMove,
  491. onStopMove: onScrollbarStopMove,
  492. spinSize: verticalScrollBarSpinSize,
  493. containerSize: size.height,
  494. style: styles === null || styles === void 0 ? void 0 : styles.verticalScrollBar,
  495. thumbStyle: styles === null || styles === void 0 ? void 0 : styles.verticalScrollBarThumb,
  496. showScrollBar: showScrollBar
  497. }), inVirtual && scrollWidth > size.width && /*#__PURE__*/React.createElement(ScrollBar, {
  498. ref: horizontalScrollBarRef,
  499. prefixCls: prefixCls,
  500. scrollOffset: offsetLeft,
  501. scrollRange: scrollWidth,
  502. rtl: isRTL,
  503. onScroll: onScrollBar,
  504. onStartMove: onScrollbarStartMove,
  505. onStopMove: onScrollbarStopMove,
  506. spinSize: horizontalScrollBarSpinSize,
  507. containerSize: size.width,
  508. horizontal: true,
  509. style: styles === null || styles === void 0 ? void 0 : styles.horizontalScrollBar,
  510. thumbStyle: styles === null || styles === void 0 ? void 0 : styles.horizontalScrollBarThumb,
  511. showScrollBar: showScrollBar
  512. }));
  513. }
  514. var List = /*#__PURE__*/React.forwardRef(RawList);
  515. List.displayName = 'List';
  516. export default List;