index.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  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 && !disabled,
  312. [`${prefixCls}-item-all-disabled`]: disabled,
  313. // [`${prefixCls}-item-rest-opening`]: (i < 3 && isLeftRestHover && page ==='...') || (i > 3 && isRightRestHover && page === '...')
  314. });
  315. const pageEl = (
  316. <li
  317. key={`${page}${i}`}
  318. onClick={() => !disabled && this.foundation.goPage(page, i)}
  319. className={pageListClassName}
  320. aria-label={page === '...' ? 'More' : `Page ${page}`}
  321. aria-current={currentPage === page ? "page" : false}
  322. >
  323. {page}
  324. </li>
  325. );
  326. if (page === '...' && !disabled) {
  327. let content;
  328. i < 3 ? (content = restLeftPageList) : (content = restRightPageList);
  329. return (
  330. <Popover
  331. trigger="hover"
  332. // onVisibleChange={visible=>this.handleRestHover(visible, i < 3 ? 'left' : 'right')}
  333. content={this.renderRestPageList(content)}
  334. key={`${page}${i}`}
  335. position={popoverPosition}
  336. zIndex={popoverZIndex}
  337. >
  338. {pageEl}
  339. </Popover>
  340. );
  341. }
  342. return pageEl;
  343. });
  344. }
  345. renderRestPageList(restList: ('...' | number)[]) {
  346. // The number of pages may be tens of thousands, here is virtualized with the help of react-window
  347. const { direction } = this.context;
  348. const className = classNames(`${prefixCls}-rest-item`);
  349. const count = restList.length;
  350. const row = (item: { index: number; style: React.CSSProperties }) => {
  351. const { index, style } = item;
  352. const page = restList[index];
  353. return (
  354. <div
  355. role="listitem"
  356. key={`${page}${index}`}
  357. className={className}
  358. onClick={() => this.foundation.goPage(page, index)}
  359. style={style}
  360. aria-label={`${page}`}
  361. >
  362. {page}
  363. </div>
  364. );
  365. };
  366. const itemHeight = 32;
  367. const listHeight = count >= 5 ? itemHeight * 5 : itemHeight * count;
  368. return (
  369. // @ts-ignore skip type check cause react-window not update with @types/react 18
  370. <List
  371. className={`${prefixCls}-rest-list`}
  372. itemData={restList}
  373. itemSize={itemHeight}
  374. width={78}
  375. itemCount={count}
  376. height={listHeight}
  377. style={{ direction }}
  378. >
  379. {row}
  380. </List>
  381. );
  382. }
  383. renderSmallPage(locale: PaginationLocale) {
  384. const { className, style, hideOnSinglePage, hoverShowPageSelect, showSizeChanger, disabled, ...rest } = this.props;
  385. const paginationCls = classNames(`${prefixCls}-small`, prefixCls, className, { [`${prefixCls}-disabled`]: disabled });
  386. const { currentPage, total, pageSize } = this.state;
  387. const totalPageNum = Math.ceil(total / pageSize);
  388. if (totalPageNum < 2 && hideOnSinglePage && !showSizeChanger) {
  389. return null;
  390. }
  391. const pageNumbers = Array.from({ length: Math.ceil(total / pageSize) }, (v, i) => i + 1);
  392. const pageList = this.renderRestPageList(pageNumbers);
  393. const pageCls = classNames({
  394. [`${prefixCls}-item`]: true,
  395. [`${prefixCls}-item-small`]: true,
  396. [`${prefixCls}-item-all-disabled`]: disabled,
  397. });
  398. const page = (<div className={pageCls}>{currentPage}/{totalPageNum} </div>);
  399. return (
  400. <div className={paginationCls} style={style} {...this.getDataAttr(rest)}>
  401. {this.renderPrevBtn()}
  402. {
  403. (hoverShowPageSelect && !disabled) ? (
  404. <Popover
  405. content={pageList}
  406. >
  407. {page}
  408. </Popover>
  409. ) : page
  410. }
  411. {this.renderNextBtn()}
  412. {this.renderQuickJump(locale)}
  413. </div>
  414. );
  415. }
  416. renderDefaultPage(locale: PaginationLocale) {
  417. const { total, pageSize } = this.state;
  418. const { showTotal, className, style, hideOnSinglePage, showSizeChanger, disabled, ...rest } = this.props;
  419. const paginationCls = classNames(className, `${prefixCls}`, { [`${prefixCls}-disabled`]: disabled });
  420. const showTotalCls = `${prefixCls}-total`;
  421. const totalPageNum = Math.ceil(total / pageSize);
  422. if (totalPageNum < 2 && hideOnSinglePage && !showSizeChanger) {
  423. return null;
  424. }
  425. const totalNum = Math.ceil(total / pageSize);
  426. const totalToken = locale.total.replace('${total}', totalNum.toString());
  427. return (
  428. <ul className={paginationCls} style={style} {...this.getDataAttr(rest)}>
  429. {showTotal ? (
  430. <span className={showTotalCls}>
  431. {totalToken}
  432. </span>
  433. ) : null}
  434. {this.renderPrevBtn()}
  435. {this.renderPageList()}
  436. {this.renderNextBtn()}
  437. {this.renderPageSizeSwitch(locale)}
  438. {this.renderQuickJump(locale)}
  439. </ul>
  440. );
  441. }
  442. render() {
  443. const { size } = this.props;
  444. return (
  445. <LocaleConsumer componentName="Pagination">
  446. {
  447. (locale: PaginationLocale) => (
  448. size === 'small' ? this.renderSmallPage(locale) : this.renderDefaultPage(locale)
  449. )
  450. }
  451. </LocaleConsumer>
  452. );
  453. }
  454. }