index.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  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. disabled?: boolean
  50. }
  51. export interface PaginationState {
  52. total: number;
  53. showTotal: boolean;
  54. currentPage: number;
  55. pageSize: number;
  56. pageList: PageList;
  57. prevDisabled: boolean;
  58. quickJumpPage: string | number;
  59. nextDisabled: boolean;
  60. restLeftPageList: number[];
  61. restRightPageList: 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. this.state = {
  111. total: props.total,
  112. showTotal: props.showTotal,
  113. currentPage: props.currentPage || props.defaultCurrentPage,
  114. 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
  115. pageList: [],
  116. prevDisabled: false,
  117. nextDisabled: false,
  118. restLeftPageList: [],
  119. restRightPageList: [],
  120. quickJumpPage: '',
  121. };
  122. this.foundation = new PaginationFoundation(this.adapter);
  123. this.renderDefaultPage = this.renderDefaultPage.bind(this);
  124. this.renderSmallPage = this.renderSmallPage.bind(this);
  125. warning(
  126. Boolean(props.showSizeChanger && props.hideOnSinglePage),
  127. '[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'
  128. );
  129. }
  130. context: ContextValue;
  131. get adapter(): PaginationAdapter<PaginationProps, PaginationState> {
  132. return {
  133. ...super.adapter,
  134. setPageList: (pageListState: AdapterPageList) => {
  135. const { pageList, restLeftPageList, restRightPageList } = pageListState;
  136. this.setState({ pageList, restLeftPageList, restRightPageList });
  137. },
  138. setDisabled: (prevIsDisabled: boolean, nextIsDisabled: boolean) => {
  139. this.setState({ prevDisabled: prevIsDisabled, nextDisabled: nextIsDisabled });
  140. },
  141. updateTotal: (total: number) => this.setState({ total }),
  142. updatePageSize: (pageSize: number) => this.setState({ pageSize }),
  143. updateQuickJumpPage: (quickJumpPage: string | number) => this.setState({ quickJumpPage }),
  144. // updateRestPageList: () => {},
  145. setCurrentPage: (pageIndex: number) => {
  146. this.setState({ currentPage: pageIndex });
  147. },
  148. registerKeyDownHandler: (handler: KeyDownHandler) => {
  149. document.addEventListener('keydown', handler);
  150. },
  151. unregisterKeyDownHandler: (handler: KeyDownHandler) => {
  152. document.removeEventListener('keydown', handler);
  153. },
  154. notifyPageChange: (pageIndex: number) => {
  155. this.props.onPageChange(pageIndex);
  156. },
  157. notifyPageSizeChange: (pageSize: number) => {
  158. this.props.onPageSizeChange(pageSize);
  159. },
  160. notifyChange: (pageIndex: number, pageSize: number) => {
  161. this.props.onChange(pageIndex, pageSize);
  162. }
  163. };
  164. }
  165. componentDidMount() {
  166. this.foundation.init();
  167. }
  168. componentWillUnmount() {
  169. this.foundation.destroy();
  170. }
  171. componentDidUpdate(prevProps: PaginationProps) {
  172. const pagerProps = {
  173. currentPage: this.props.currentPage,
  174. total: this.props.total,
  175. pageSize: this.props.pageSize,
  176. };
  177. let pagerHasChanged = false;
  178. if (prevProps.currentPage !== this.props.currentPage) {
  179. pagerHasChanged = true;
  180. // this.foundation.updatePage(this.props.currentPage);
  181. }
  182. if (prevProps.total !== this.props.total) {
  183. pagerHasChanged = true;
  184. }
  185. if (prevProps.pageSize !== this.props.pageSize) {
  186. pagerHasChanged = true;
  187. }
  188. if (pagerHasChanged) {
  189. this.foundation.updatePage(pagerProps.currentPage, pagerProps.total, pagerProps.pageSize);
  190. }
  191. }
  192. renderPrevBtn() {
  193. const { prevText, disabled } = this.props;
  194. const { prevDisabled } = this.state;
  195. const isDisabled = prevDisabled || disabled;
  196. const preClassName = classNames({
  197. [`${prefixCls}-item`]: true,
  198. [`${prefixCls}-prev`]: true,
  199. [`${prefixCls}-item-disabled`]: isDisabled,
  200. });
  201. return (
  202. <li
  203. role="button"
  204. aria-disabled={isDisabled ? true : false}
  205. aria-label="Previous"
  206. onClick={e => !isDisabled && this.foundation.goPrev(e)}
  207. className={preClassName}
  208. x-semi-prop="prevText"
  209. >
  210. {prevText || <IconChevronLeft size="large" />}
  211. </li>
  212. );
  213. }
  214. renderNextBtn() {
  215. const { nextText, disabled } = this.props;
  216. const { nextDisabled } = this.state;
  217. const isDisabled = nextDisabled || disabled;
  218. const nextClassName = classNames({
  219. [`${prefixCls}-item`]: true,
  220. [`${prefixCls}-item-disabled`]: isDisabled,
  221. [`${prefixCls}-next`]: true,
  222. });
  223. return (
  224. <li
  225. role="button"
  226. aria-disabled={isDisabled ? true : false}
  227. aria-label="Next"
  228. onClick={e => !isDisabled && this.foundation.goNext(e)}
  229. className={nextClassName}
  230. x-semi-prop="prevText"
  231. >
  232. {nextText || <IconChevronRight size="large" />}
  233. </li>
  234. );
  235. }
  236. renderPageSizeSwitch(locale: PaginationLocale) {
  237. // rtl modify the default position
  238. const { direction } = this.context;
  239. const defaultPopoverPosition = direction === 'rtl' ? 'bottomRight' : 'bottomLeft';
  240. const { showSizeChanger, popoverPosition = defaultPopoverPosition, disabled } = this.props;
  241. const { pageSize } = this.state;
  242. const switchCls = classNames(`${prefixCls}-switch`);
  243. if (!showSizeChanger) {
  244. return null;
  245. }
  246. const newPageSizeOpts = this.foundation.pageSizeInOpts();
  247. const pageSizeToken = locale.pageSize;
  248. // Display pageSize in a specific language format order
  249. const options = newPageSizeOpts.map((size: number) => (
  250. <Option value={size} key={size}>
  251. <span>
  252. {pageSizeToken.replace('${pageSize}', size.toString())}
  253. </span>
  254. </Option>
  255. ));
  256. return (
  257. <div className={switchCls}>
  258. <Select
  259. aria-label="Page size selector"
  260. disabled={disabled}
  261. onChange={newPageSize => this.foundation.changePageSize(newPageSize)}
  262. value={pageSize}
  263. key={pageSize}
  264. position={popoverPosition || 'bottomRight'}
  265. clickToHide
  266. dropdownClassName={`${prefixCls}-select-dropdown`}
  267. >
  268. {options}
  269. </Select>
  270. </div>
  271. );
  272. }
  273. renderQuickJump(locale: PaginationLocale) {
  274. const { showQuickJumper, disabled } = this.props;
  275. const { quickJumpPage, total, pageSize } = this.state;
  276. if (!showQuickJumper) {
  277. return null;
  278. }
  279. const totalPageNum = this.foundation._getTotalPageNumber(total, pageSize);
  280. const isDisabled = (totalPageNum === 1) || disabled;
  281. const quickJumpCls = classNames({
  282. [`${prefixCls}-quickjump`]: true,
  283. [`${prefixCls}-quickjump-disabled`]: isDisabled
  284. });
  285. return (
  286. <div className={quickJumpCls}>
  287. <span>{locale.jumpTo}</span>
  288. <InputNumber
  289. value={quickJumpPage}
  290. className={`${prefixCls}-quickjump-input-number`}
  291. hideButtons
  292. disabled={isDisabled}
  293. onBlur={(e: React.FocusEvent) => this.foundation.handleQuickJumpBlur()}
  294. onEnterPress={(e: React.KeyboardEvent) => this.foundation.handleQuickJumpEnterPress((e.target as any).value)}
  295. onChange={(v: string | number) => this.foundation.handleQuickJumpNumberChange(v)}
  296. />
  297. <span>{locale.page}</span>
  298. </div>
  299. );
  300. }
  301. renderPageList() {
  302. const {
  303. pageList,
  304. currentPage,
  305. restLeftPageList,
  306. restRightPageList,
  307. } = this.state;
  308. const { popoverPosition, popoverZIndex, disabled } = this.props;
  309. return pageList.map((page, i) => {
  310. const pageListClassName = classNames(`${prefixCls}-item`, {
  311. [`${prefixCls}-item-active`]: currentPage === page,
  312. [`${prefixCls}-item-all-disabled`]: disabled,
  313. [`${prefixCls}-item-all-disabled-active`]: currentPage === page && disabled,
  314. // [`${prefixCls}-item-rest-opening`]: (i < 3 && isLeftRestHover && page ==='...') || (i > 3 && isRightRestHover && page === '...')
  315. });
  316. const pageEl = (
  317. <li
  318. key={`${page}${i}`}
  319. onClick={() => !disabled && this.foundation.goPage(page, i)}
  320. className={pageListClassName}
  321. aria-label={page === '...' ? 'More' : `Page ${page}`}
  322. aria-current={currentPage === page ? "page" : false}
  323. >
  324. {page}
  325. </li>
  326. );
  327. if (page === '...' && !disabled) {
  328. let content;
  329. i < 3 ? (content = restLeftPageList) : (content = restRightPageList);
  330. return (
  331. <Popover
  332. trigger="hover"
  333. // onVisibleChange={visible=>this.handleRestHover(visible, i < 3 ? 'left' : 'right')}
  334. content={this.renderRestPageList(content)}
  335. key={`${page}${i}`}
  336. position={popoverPosition}
  337. zIndex={popoverZIndex}
  338. >
  339. {pageEl}
  340. </Popover>
  341. );
  342. }
  343. return pageEl;
  344. });
  345. }
  346. renderRestPageList(restList: ('...' | number)[]) {
  347. // The number of pages may be tens of thousands, here is virtualized with the help of react-window
  348. const { direction } = this.context;
  349. const className = classNames(`${prefixCls}-rest-item`);
  350. const count = restList.length;
  351. const row = (item: { index: number; style: React.CSSProperties }) => {
  352. const { index, style } = item;
  353. const page = restList[index];
  354. return (
  355. <div
  356. role="listitem"
  357. key={`${page}${index}`}
  358. className={className}
  359. onClick={() => this.foundation.goPage(page, index)}
  360. style={style}
  361. aria-label={`${page}`}
  362. >
  363. {page}
  364. </div>
  365. );
  366. };
  367. const itemHeight = 32;
  368. const listHeight = count >= 5 ? itemHeight * 5 : itemHeight * count;
  369. return (
  370. // @ts-ignore skip type check cause react-window not update with @types/react 18
  371. <List
  372. className={`${prefixCls}-rest-list`}
  373. itemData={restList}
  374. itemSize={itemHeight}
  375. width={78}
  376. itemCount={count}
  377. height={listHeight}
  378. style={{ direction }}
  379. >
  380. {row}
  381. </List>
  382. );
  383. }
  384. renderSmallPage(locale: PaginationLocale) {
  385. const { className, style, hideOnSinglePage, hoverShowPageSelect, showSizeChanger, disabled, ...rest } = this.props;
  386. const paginationCls = classNames(`${prefixCls}-small`, prefixCls, className, { [`${prefixCls}-disabled`]: disabled });
  387. const { currentPage, total, pageSize } = this.state;
  388. const totalPageNum = Math.ceil(total / pageSize);
  389. if (totalPageNum < 2 && hideOnSinglePage && !showSizeChanger) {
  390. return null;
  391. }
  392. const pageNumbers = Array.from({ length: Math.ceil(total / pageSize) }, (v, i) => i + 1);
  393. const pageList = this.renderRestPageList(pageNumbers);
  394. const pageCls = classNames({
  395. [`${prefixCls}-item`]: true,
  396. [`${prefixCls}-item-small`]: true,
  397. [`${prefixCls}-item-all-disabled`]: disabled,
  398. });
  399. const page = (<div className={pageCls}>{currentPage}/{totalPageNum} </div>);
  400. return (
  401. <div className={paginationCls} style={style} {...this.getDataAttr(rest)}>
  402. {this.renderPrevBtn()}
  403. {
  404. (hoverShowPageSelect && !disabled) ? (
  405. <Popover
  406. content={pageList}
  407. >
  408. {page}
  409. </Popover>
  410. ) : page
  411. }
  412. {this.renderNextBtn()}
  413. {this.renderQuickJump(locale)}
  414. </div>
  415. );
  416. }
  417. renderDefaultPage(locale: PaginationLocale) {
  418. const { total, pageSize } = this.state;
  419. const { showTotal, className, style, hideOnSinglePage, showSizeChanger, disabled, ...rest } = this.props;
  420. const paginationCls = classNames(className, `${prefixCls}`, { [`${prefixCls}-disabled`]: disabled });
  421. const showTotalCls = `${prefixCls}-total`;
  422. const totalPageNum = Math.ceil(total / pageSize);
  423. if (totalPageNum < 2 && hideOnSinglePage && !showSizeChanger) {
  424. return null;
  425. }
  426. const totalNum = Math.ceil(total / pageSize);
  427. const totalToken = locale.total.replace('${total}', totalNum.toString());
  428. return (
  429. <ul className={paginationCls} style={style} {...this.getDataAttr(rest)}>
  430. {showTotal ? (
  431. <span className={showTotalCls}>
  432. {totalToken}
  433. </span>
  434. ) : null}
  435. {this.renderPrevBtn()}
  436. {this.renderPageList()}
  437. {this.renderNextBtn()}
  438. {this.renderPageSizeSwitch(locale)}
  439. {this.renderQuickJump(locale)}
  440. </ul>
  441. );
  442. }
  443. render() {
  444. const { size } = this.props;
  445. return (
  446. <LocaleConsumer componentName="Pagination">
  447. {
  448. (locale: PaginationLocale) => (
  449. size === 'small' ? this.renderSmallPage(locale) : this.renderDefaultPage(locale)
  450. )
  451. }
  452. </LocaleConsumer>
  453. );
  454. }
  455. }