index.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. /* eslint-disable jsx-a11y/no-noninteractive-element-to-interactive-role */
  2. import React from 'react';
  3. import classNames from 'classnames';
  4. import PropTypes from 'prop-types';
  5. import { FixedSizeList as List } from 'react-window';
  6. import { noop } from 'lodash';
  7. import PaginationFoundation, {
  8. AdapterPageList,
  9. KeyDownHandler,
  10. PageList, PaginationAdapter
  11. } from '@douyinfe/semi-foundation/pagination/foundation';
  12. import { cssClasses, numbers } from '@douyinfe/semi-foundation/pagination/constants';
  13. import '@douyinfe/semi-foundation/pagination/pagination.scss';
  14. import { numbers as popoverNumbers } from '@douyinfe/semi-foundation/popover/constants';
  15. import { IconChevronLeft, IconChevronRight } from '@douyinfe/semi-icons';
  16. import warning from '@douyinfe/semi-foundation/utils/warning';
  17. import ConfigContext, { ContextValue } from '../configProvider/context';
  18. import LocaleConsumer from '../locale/localeConsumer';
  19. import { Locale } from '../locale/interface';
  20. import Select from '../select/index';
  21. import InputNumber from '../inputNumber/index';
  22. import BaseComponent from '../_base/baseComponent';
  23. import Popover from '../popover/index';
  24. import { Position } from '../tooltip';
  25. const prefixCls = cssClasses.PREFIX;
  26. const { Option } = Select;
  27. export interface PaginationProps {
  28. total?: number;
  29. showTotal?: boolean;
  30. pageSize?: number;
  31. pageSizeOpts?: Array<number>;
  32. size?: 'small' | 'default';
  33. currentPage?: number;
  34. defaultCurrentPage?: number;
  35. onPageChange?: (currentPage: number) => void;
  36. onPageSizeChange?: (newPageSize: number) => void;
  37. onChange?: (currentPage: number, pageSize: number) => void;
  38. prevText?: React.ReactNode;
  39. nextText?: React.ReactNode;
  40. showSizeChanger?: boolean;
  41. showQuickJumper?: boolean;
  42. popoverZIndex?: number;
  43. popoverPosition?: PopoverPosition;
  44. style?: React.CSSProperties;
  45. className?: string;
  46. hideOnSinglePage?: boolean;
  47. hoverShowPageSelect?: boolean;
  48. disabled?: boolean
  49. }
  50. export interface PaginationState {
  51. total: number;
  52. showTotal: boolean;
  53. currentPage: number;
  54. pageSize: number;
  55. pageList: PageList;
  56. prevDisabled: boolean;
  57. quickJumpPage: string | number;
  58. nextDisabled: boolean;
  59. restLeftPageList: number[];
  60. restRightPageList: number[];
  61. allPageNumbers: number[]
  62. }
  63. export type PaginationLocale = Locale['Pagination'];
  64. export type PopoverPosition = Position;
  65. export type { PageList };
  66. export default class Pagination extends BaseComponent<PaginationProps, PaginationState> {
  67. static contextType = ConfigContext;
  68. static propTypes = {
  69. total: PropTypes.number,
  70. showTotal: PropTypes.bool,
  71. pageSize: PropTypes.number,
  72. pageSizeOpts: PropTypes.array,
  73. size: PropTypes.string,
  74. currentPage: PropTypes.number,
  75. defaultCurrentPage: PropTypes.number,
  76. onPageChange: PropTypes.func,
  77. onPageSizeChange: PropTypes.func,
  78. onChange: PropTypes.func,
  79. prevText: PropTypes.node,
  80. nextText: PropTypes.node,
  81. showSizeChanger: PropTypes.bool,
  82. popoverZIndex: PropTypes.number,
  83. popoverPosition: PropTypes.string,
  84. style: PropTypes.object,
  85. className: PropTypes.string,
  86. hideOnSinglePage: PropTypes.bool,
  87. hoverShowPageSelect: PropTypes.bool,
  88. showQuickJumper: PropTypes.bool,
  89. disabled: PropTypes.bool,
  90. };
  91. static defaultProps = {
  92. total: 1,
  93. popoverZIndex: popoverNumbers.DEFAULT_Z_INDEX,
  94. showTotal: false,
  95. pageSize: null as null,
  96. pageSizeOpts: numbers.PAGE_SIZE_OPTION,
  97. defaultCurrentPage: 1,
  98. size: 'default',
  99. onPageChange: noop,
  100. onPageSizeChange: noop,
  101. onChange: noop,
  102. showSizeChanger: false,
  103. className: '',
  104. hideOnSinglePage: false,
  105. showQuickJumper: false,
  106. disabled: false,
  107. };
  108. constructor(props: PaginationProps) {
  109. super(props);
  110. const total = props.total;
  111. const pageSize = props.pageSize || props.pageSizeOpts[0] || numbers.DEFAULT_PAGE_SIZE; // Use pageSize first, use the first of pageSizeOpts when not, use the default value when none
  112. const shouldFillAllNumber = props.size === 'small' && props.hoverShowPageSelect && !props.disabled;
  113. this.state = {
  114. total,
  115. showTotal: props.showTotal,
  116. currentPage: props.currentPage || props.defaultCurrentPage,
  117. pageSize,
  118. pageList: [],
  119. prevDisabled: false,
  120. nextDisabled: false,
  121. restLeftPageList: [],
  122. restRightPageList: [],
  123. quickJumpPage: '',
  124. allPageNumbers: shouldFillAllNumber ? Array.from({ length: Math.ceil(total / pageSize) }, (v, i) => i + 1) : [], // only need to count in smallPage mode, when props.size = small
  125. };
  126. this.foundation = new PaginationFoundation(this.adapter);
  127. this.renderDefaultPage = this.renderDefaultPage.bind(this);
  128. this.renderSmallPage = this.renderSmallPage.bind(this);
  129. warning(
  130. Boolean(props.showSizeChanger && props.hideOnSinglePage),
  131. '[Semi Pagination] You should not use showSizeChanger and hideOnSinglePage in ths same time. At this time, hideOnSinglePage no longer takes effect, otherwise there may be a problem that the switch entry disappears'
  132. );
  133. }
  134. context: ContextValue;
  135. get adapter(): PaginationAdapter<PaginationProps, PaginationState> {
  136. return {
  137. ...super.adapter,
  138. setPageList: (pageListState: AdapterPageList) => {
  139. const { pageList, restLeftPageList, restRightPageList } = pageListState;
  140. this.setState({ pageList, restLeftPageList, restRightPageList });
  141. },
  142. setDisabled: (prevIsDisabled: boolean, nextIsDisabled: boolean) => {
  143. this.setState({ prevDisabled: prevIsDisabled, nextDisabled: nextIsDisabled });
  144. },
  145. updateTotal: (total: number) => this.setState({ total }),
  146. updatePageSize: (pageSize: number) => this.setState({ pageSize }),
  147. updateQuickJumpPage: (quickJumpPage: string | number) => this.setState({ quickJumpPage }),
  148. updateAllPageNumbers: (allPageNumbers: number[]) => this.setState({ allPageNumbers }),
  149. setCurrentPage: (pageIndex: number) => {
  150. this.setState({ currentPage: pageIndex });
  151. },
  152. registerKeyDownHandler: (handler: KeyDownHandler) => {
  153. document.addEventListener('keydown', handler);
  154. },
  155. unregisterKeyDownHandler: (handler: KeyDownHandler) => {
  156. document.removeEventListener('keydown', handler);
  157. },
  158. notifyPageChange: (pageIndex: number) => {
  159. this.props.onPageChange(pageIndex);
  160. },
  161. notifyPageSizeChange: (pageSize: number) => {
  162. this.props.onPageSizeChange(pageSize);
  163. },
  164. notifyChange: (pageIndex: number, pageSize: number) => {
  165. this.props.onChange(pageIndex, pageSize);
  166. }
  167. };
  168. }
  169. componentDidMount() {
  170. this.foundation.init();
  171. }
  172. componentWillUnmount() {
  173. this.foundation.destroy();
  174. }
  175. componentDidUpdate(prevProps: PaginationProps) {
  176. const pagerProps = {
  177. currentPage: this.props.currentPage,
  178. total: this.props.total,
  179. pageSize: this.props.pageSize,
  180. };
  181. let pagerHasChanged = false;
  182. let allPageNumberNeedUpdate = false;
  183. if (prevProps.currentPage !== this.props.currentPage) {
  184. pagerHasChanged = true;
  185. // this.foundation.updatePage(this.props.currentPage);
  186. }
  187. if (prevProps.total !== this.props.total) {
  188. pagerHasChanged = true;
  189. allPageNumberNeedUpdate = true;
  190. }
  191. if (prevProps.pageSize !== this.props.pageSize) {
  192. pagerHasChanged = true;
  193. allPageNumberNeedUpdate = true;
  194. }
  195. if (pagerHasChanged) {
  196. this.foundation.updatePage(pagerProps.currentPage, pagerProps.total, pagerProps.pageSize);
  197. }
  198. if (allPageNumberNeedUpdate) {
  199. this.foundation.updateAllPageNumbers(pagerProps.total, pagerProps.pageSize);
  200. }
  201. }
  202. renderPrevBtn() {
  203. const { prevText, disabled } = this.props;
  204. const { prevDisabled } = this.state;
  205. const isDisabled = prevDisabled || disabled;
  206. const preClassName = classNames({
  207. [`${prefixCls}-item`]: true,
  208. [`${prefixCls}-prev`]: true,
  209. [`${prefixCls}-item-disabled`]: isDisabled,
  210. });
  211. return (
  212. <li
  213. role="button"
  214. aria-disabled={isDisabled ? true : false}
  215. aria-label="Previous"
  216. onClick={e => !isDisabled && this.foundation.goPrev(e)}
  217. className={preClassName}
  218. x-semi-prop="prevText"
  219. >
  220. {prevText || <IconChevronLeft size="large" />}
  221. </li>
  222. );
  223. }
  224. renderNextBtn() {
  225. const { nextText, disabled } = this.props;
  226. const { nextDisabled } = this.state;
  227. const isDisabled = nextDisabled || disabled;
  228. const nextClassName = classNames({
  229. [`${prefixCls}-item`]: true,
  230. [`${prefixCls}-item-disabled`]: isDisabled,
  231. [`${prefixCls}-next`]: true,
  232. });
  233. return (
  234. <li
  235. role="button"
  236. aria-disabled={isDisabled ? true : false}
  237. aria-label="Next"
  238. onClick={e => !isDisabled && this.foundation.goNext(e)}
  239. className={nextClassName}
  240. x-semi-prop="nextText"
  241. >
  242. {nextText || <IconChevronRight size="large" />}
  243. </li>
  244. );
  245. }
  246. renderPageSizeSwitch(locale: PaginationLocale) {
  247. // rtl modify the default position
  248. const { direction } = this.context;
  249. const defaultPopoverPosition = direction === 'rtl' ? 'bottomRight' : 'bottomLeft';
  250. const { showSizeChanger, popoverPosition = defaultPopoverPosition, disabled, popoverZIndex } = this.props;
  251. const { pageSize } = this.state;
  252. const switchCls = classNames(`${prefixCls}-switch`);
  253. if (!showSizeChanger) {
  254. return null;
  255. }
  256. const newPageSizeOpts = this.foundation.pageSizeInOpts();
  257. const pageSizeToken = locale.pageSize;
  258. // Display pageSize in a specific language format order
  259. const options = newPageSizeOpts.map((size: number) => (
  260. <Option value={size} key={size}>
  261. <span>
  262. {pageSizeToken.replace('${pageSize}', size.toString())}
  263. </span>
  264. </Option>
  265. ));
  266. return (
  267. <div className={switchCls}>
  268. <Select
  269. aria-label="Page size selector"
  270. disabled={disabled}
  271. onChange={newPageSize => this.foundation.changePageSize(newPageSize)}
  272. value={pageSize}
  273. key={pageSize + pageSizeToken}
  274. position={popoverPosition || 'bottomRight'}
  275. clickToHide
  276. zIndex={popoverZIndex}
  277. dropdownClassName={`${prefixCls}-select-dropdown`}
  278. >
  279. {options}
  280. </Select>
  281. </div>
  282. );
  283. }
  284. renderQuickJump(locale: PaginationLocale) {
  285. const { showQuickJumper, disabled } = this.props;
  286. const { quickJumpPage, total, pageSize } = this.state;
  287. if (!showQuickJumper) {
  288. return null;
  289. }
  290. const totalPageNum = this.foundation._getTotalPageNumber(total, pageSize);
  291. const isDisabled = (totalPageNum === 1) || disabled;
  292. const quickJumpCls = classNames({
  293. [`${prefixCls}-quickjump`]: true,
  294. [`${prefixCls}-quickjump-disabled`]: isDisabled
  295. });
  296. return (
  297. <div className={quickJumpCls}>
  298. <span>{locale.jumpTo}</span>
  299. <InputNumber
  300. value={quickJumpPage}
  301. className={`${prefixCls}-quickjump-input-number`}
  302. hideButtons
  303. disabled={isDisabled}
  304. onBlur={(e: React.FocusEvent) => this.foundation.handleQuickJumpBlur()}
  305. onEnterPress={(e: React.KeyboardEvent) => this.foundation.handleQuickJumpEnterPress((e.target as any).value)}
  306. onChange={(v: string | number) => this.foundation.handleQuickJumpNumberChange(v)}
  307. />
  308. <span>{locale.page}</span>
  309. </div>
  310. );
  311. }
  312. renderPageList() {
  313. const {
  314. pageList,
  315. currentPage,
  316. restLeftPageList,
  317. restRightPageList,
  318. } = this.state;
  319. const { popoverPosition, popoverZIndex, disabled } = this.props;
  320. return pageList.map((page, i) => {
  321. const pageListClassName = classNames(`${prefixCls}-item`, {
  322. [`${prefixCls}-item-active`]: currentPage === page,
  323. [`${prefixCls}-item-all-disabled`]: disabled,
  324. [`${prefixCls}-item-all-disabled-active`]: currentPage === page && disabled,
  325. // [`${prefixCls}-item-rest-opening`]: (i < 3 && isLeftRestHover && page ==='...') || (i > 3 && isRightRestHover && page === '...')
  326. });
  327. const pageEl = (
  328. <li
  329. key={`${page}${i}`}
  330. onClick={() => !disabled && this.foundation.goPage(page, i)}
  331. className={pageListClassName}
  332. aria-label={page === '...' ? 'More' : `Page ${page}`}
  333. aria-current={currentPage === page ? "page" : false}
  334. >
  335. {page}
  336. </li>
  337. );
  338. if (page === '...' && !disabled) {
  339. let content;
  340. i < 3 ? (content = restLeftPageList) : (content = restRightPageList);
  341. return (
  342. <Popover
  343. rePosKey={this.props.currentPage}
  344. trigger="hover"
  345. // onVisibleChange={visible=>this.handleRestHover(visible, i < 3 ? 'left' : 'right')}
  346. content={this.renderRestPageList(content)}
  347. key={`${page}${i}`}
  348. position={popoverPosition}
  349. zIndex={popoverZIndex}
  350. >
  351. {pageEl}
  352. </Popover>
  353. );
  354. }
  355. return pageEl;
  356. });
  357. }
  358. renderRestPageList(restList: ('...' | number)[]) {
  359. // The number of pages may be tens of thousands, here is virtualized with the help of react-window
  360. const { direction } = this.context;
  361. const className = classNames(`${prefixCls}-rest-item`);
  362. const count = restList.length;
  363. const row = (item: { index: number; style: React.CSSProperties }) => {
  364. const { index, style } = item;
  365. const page = restList[index];
  366. return (
  367. <div
  368. role="listitem"
  369. key={`${page}${index}`}
  370. className={className}
  371. onClick={() => this.foundation.goPage(page, index)}
  372. style={style}
  373. aria-label={`${page}`}
  374. >
  375. {page}
  376. </div>
  377. );
  378. };
  379. const itemHeight = 32;
  380. const listHeight = count >= 5 ? itemHeight * 5 : itemHeight * count;
  381. return (
  382. // @ts-ignore skip type check cause react-window not update with @types/react 18
  383. <List
  384. className={`${prefixCls}-rest-list`}
  385. itemData={restList}
  386. itemSize={itemHeight}
  387. width={78}
  388. itemCount={count}
  389. height={listHeight}
  390. style={{ direction }}
  391. >
  392. {row}
  393. </List>
  394. );
  395. }
  396. renderSmallPageSelect(content: React.ReactNode) {
  397. const allPageNumbers = this.state.allPageNumbers;
  398. const pageList = this.renderRestPageList(allPageNumbers);
  399. return (
  400. <Popover
  401. content={pageList}
  402. >
  403. {content}
  404. </Popover>
  405. );
  406. }
  407. renderSmallPage(locale: PaginationLocale) {
  408. const { className, style, hideOnSinglePage, hoverShowPageSelect, showSizeChanger, disabled, ...rest } = this.props;
  409. const paginationCls = classNames(`${prefixCls}-small`, prefixCls, className, { [`${prefixCls}-disabled`]: disabled });
  410. const { currentPage, total, pageSize } = this.state;
  411. const totalPageNum = Math.ceil(total / pageSize);
  412. if (totalPageNum < 2 && hideOnSinglePage && !showSizeChanger) {
  413. return null;
  414. }
  415. const pageCls = classNames({
  416. [`${prefixCls}-item`]: true,
  417. [`${prefixCls}-item-small`]: true,
  418. [`${prefixCls}-item-all-disabled`]: disabled,
  419. });
  420. const content = (<div className={pageCls}>{currentPage}/{totalPageNum} </div>);
  421. return (
  422. <div className={paginationCls} style={style} {...this.getDataAttr(rest)}>
  423. {this.renderPrevBtn()}
  424. {
  425. (hoverShowPageSelect && !disabled) ? this.renderSmallPageSelect(content) : content
  426. }
  427. {this.renderNextBtn()}
  428. {this.renderQuickJump(locale)}
  429. </div>
  430. );
  431. }
  432. renderDefaultPage(locale: PaginationLocale) {
  433. const { total, pageSize } = this.state;
  434. const { showTotal, className, style, hideOnSinglePage, showSizeChanger, disabled, ...rest } = this.props;
  435. const paginationCls = classNames(className, `${prefixCls}`, { [`${prefixCls}-disabled`]: disabled });
  436. const showTotalCls = `${prefixCls}-total`;
  437. const totalPageNum = Math.ceil(total / pageSize);
  438. if (totalPageNum < 2 && hideOnSinglePage && !showSizeChanger) {
  439. return null;
  440. }
  441. const totalNum = Math.ceil(total / pageSize);
  442. const totalToken = locale.total.replace('${total}', totalNum.toString());
  443. return (
  444. <ul className={paginationCls} style={style} {...this.getDataAttr(rest)}>
  445. {showTotal ? (
  446. <span className={showTotalCls}>
  447. {totalToken}
  448. </span>
  449. ) : null}
  450. {this.renderPrevBtn()}
  451. {this.renderPageList()}
  452. {this.renderNextBtn()}
  453. {this.renderPageSizeSwitch(locale)}
  454. {this.renderQuickJump(locale)}
  455. </ul>
  456. );
  457. }
  458. render() {
  459. const { size } = this.props;
  460. return (
  461. <LocaleConsumer componentName="Pagination">
  462. {
  463. (locale: PaginationLocale) => (
  464. size === 'small' ? this.renderSmallPage(locale) : this.renderDefaultPage(locale)
  465. )
  466. }
  467. </LocaleConsumer>
  468. );
  469. }
  470. }