index.tsx 17 KB

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