Base UI分页组件:Pagination的实现方案
Base UI分页组件:Pagination的实现方案
【免费下载链接】base-ui Base UI is a library of headless (\"unstyled\") React components and low-level hooks. You gain complete control over your app\'s CSS and accessibility features. 项目地址: https://gitcode.com/GitHub_Trending/ba/base-ui
引言
在现代Web应用中,分页(Pagination)是处理大量数据展示的核心组件。无论是电商平台的产品列表、内容管理系统的文章列表,还是数据分析平台的结果展示,分页组件都扮演着至关重要的角色。Base UI作为无样式(headless)的React组件库,为开发者提供了构建高度定制化分页组件的完美基础。
本文将深入探讨基于Base UI架构的分页组件实现方案,涵盖设计理念、核心功能、API设计和最佳实践。
分页组件的核心需求
功能需求矩阵
分页模式对比
Base UI分页组件架构设计
核心Hook:usePagination
interface UsePaginationParameters { /** 总数据条数 */ total: number /** 当前页码 */ page?: number /** 每页显示条数 */ pageSize?: number /** 是否禁用 */ disabled?: boolean /** 可见页码数量 */ siblingCount?: number /** 边界页码数量 */ boundaryCount?: number /** 变化回调 */ onChange?: (page: number, pageSize: number) => void}interface UsePaginationReturnValue { /** 分页项数组 */ items: PaginationItem[] /** 当前页码 */ page: number /** 总页数 */ totalPages: number /** 分页属性获取器 */ getPaginationProps: (externalProps?: HTMLProps) => HTMLProps /** 分页项属性获取器 */ getItemProps: (item: PaginationItem, externalProps?: HTMLProps) => HTMLProps}interface PaginationItem { type: \'page\' | \'previous\' | \'next\' | \'first\' | \'last\' | \'ellipsis\' page?: number disabled?: boolean selected?: boolean}
分页算法实现
function generatePaginationItems( currentPage: number, totalPages: number, siblingCount: number = 1, boundaryCount: number = 1): PaginationItem[] { const items: PaginationItem[] = [] // 添加首页按钮 if (boundaryCount > 0 && currentPage > 1) { items.push({ type: \'first\', page: 1 }) } // 添加上一页按钮 if (currentPage > 1) { items.push({ type: \'previous\', page: currentPage - 1 }) } // 计算页码范围 const startPages = Array.from({ length: boundaryCount }, (_, i) => i + 1) const endPages = Array.from({ length: boundaryCount }, (_, i) => totalPages - i).reverse() const siblingsStart = Math.max( boundaryCount + 1, currentPage - siblingCount ) const siblingsEnd = Math.min( totalPages - boundaryCount, currentPage + siblingCount ) // 添加起始页码 items.push(...startPages.map(page => ({ type: \'page\', page, selected: page === currentPage }))) // 添加省略号(如果需要) if (siblingsStart > boundaryCount + 1) { items.push({ type: \'ellipsis\' }) } // 添加中间页码 for (let page = siblingsStart; page boundaryCount && page <= totalPages - boundaryCount) { items.push({ type: \'page\', page, selected: page === currentPage }) } } // 添加省略号(如果需要) if (siblingsEnd ({ type: \'page\', page, selected: page === currentPage }))) // 添加下一页按钮 if (currentPage 0 && currentPage < totalPages) { items.push({ type: \'last\', page: totalPages }) } return items}
完整组件实现
Pagination Root组件
import * as React from \'react\'import { usePagination } from \'./usePagination\'import { mergeProps } from \'../merge-props\'interface PaginationRootProps extends UsePaginationParameters { children?: React.ReactNode render?: (props: UsePaginationReturnValue) => React.ReactNode}const PaginationRoot = React.forwardRef( function PaginationRoot(props, ref) { const { total, page: controlledPage, pageSize = 10, disabled = false, siblingCount = 1, boundaryCount = 1, onChange, children, render, ...otherProps } = props const [internalPage, setInternalPage] = React.useState(1) const page = controlledPage ?? internalPage const pagination = usePagination({ total, page, pageSize, disabled, siblingCount, boundaryCount, onChange: (newPage, newPageSize) => { if (!controlledPage) { setInternalPage(newPage) } onChange?.(newPage, newPageSize) } }) const getRootProps = React.useCallback( (externalProps: React.HTMLAttributes = {}) => { return mergeProps( { role: \'navigation\', \'aria-label\': \'分页导航\', ref, }, externalProps, otherProps ) }, [ref, otherProps] ) const contextValue = React.useMemo(() => pagination, [pagination]) if (render) { return render(pagination) as React.ReactElement } return ( {children} ) })
Pagination Item组件
interface PaginationItemProps { item: PaginationItem children?: React.ReactNode render?: (props: { item: PaginationItem getItemProps: (externalProps?: HTMLProps) => HTMLProps }) => React.ReactNode}const PaginationItem = React.forwardRef( function PaginationItem(props, ref) { const { item, children, render, ...otherProps } = props const pagination = React.useContext(PaginationContext) if (!pagination) { throw new Error(\'PaginationItem must be used within PaginationRoot\') } const handleClick = React.useCallback(() => { if (item.page !== undefined && !item.disabled) { pagination.onChange?.(item.page, pagination.pageSize) } }, [item, pagination]) const getItemProps = React.useCallback( (externalProps: HTMLProps = {}) => { const baseProps: HTMLProps = { \'aria-current\': item.selected ? \'page\' : undefined, \'aria-disabled\': item.disabled || undefined, \'aria-label\': getAriaLabel(item), onClick: handleClick, ref, tabIndex: item.disabled ? -1 : 0, } return mergeProps(baseProps, externalProps, otherProps) }, [item, handleClick, ref, otherProps] ) if (render) { return render({ item, getItemProps }) as React.ReactElement } return ( ) })function getAriaLabel(item: PaginationItem): string { switch (item.type) { case \'first\': return \'跳转到首页\' case \'previous\': return \'跳转到上一页\' case \'next\': return \'跳转到下一页\' case \'last\': return \'跳转到末页\' case \'ellipsis\': return \'更多页码\' case \'page\': return `跳转到第 ${item.page} 页` default: return \'\' }}function getItemContent(item: PaginationItem): React.ReactNode { switch (item.type) { case \'first\': return \'«\' case \'previous\': return \'‹\' case \'next\': return \'›\' case \'last\': return \'»\' case \'ellipsis\': return \'…\' case \'page\': return item.page default: return null }}
使用示例
基础用法
import { PaginationRoot, PaginationItem } from \'@base-ui/react/pagination\'function DataTable({ data, total }) { const [page, setPage] = React.useState(1) const [pageSize, setPageSize] = React.useState(10) return ( {/* 数据表格内容 */} { setPage(newPage) setPageSize(newPageSize) }} render={({ items, getPaginationProps }) => ( )} /> )}
高级定制示例
function CustomPagination() { return ( ( 第 {page} 页,共 {totalPages} 页 {items.map((item, index) => ( { const props = getItemProps() return ( {item.type === \'ellipsis\' ? ( ) : ( getItemContent(item))} ) }} /> ))} setPageSize(newPageSize)} /> )} /> )}
无障碍访问支持
ARIA属性实现
const getPaginationProps = (externalProps: HTMLProps = {}) => { return mergeProps( { role: \'navigation\', \'aria-label\': \'分页导航\', \'aria-current\': \'page\', }, externalProps )}const getItemProps = (item: PaginationItem, externalProps: HTMLProps = {}) => { const baseProps: HTMLProps = { \'aria-current\': item.selected ? \'page\' : undefined, \'aria-disabled\': item.disabled || undefined, \'aria-label\': getAriaLabel(item), role: item.type === \'ellipsis\' ? \'presentation\' : \'button\', tabIndex: item.disabled ? -1 : 0, } return mergeProps(baseProps, externalProps)}
键盘导航支持
const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => { switch (event.key) { case \'ArrowLeft\': event.preventDefault() // 导航到上一页 break case \'ArrowRight\': event.preventDefault() // 导航到下一页 break case \'Home\': event.preventDefault() // 导航到首页 break case \'End\': event.preventDefault() // 导航到末页 break case \'Enter\': case \'Space\': if (item.type !== \'ellipsis\') { event.preventDefault() handleClick() } break }}, [item, handleClick])
性能优化策略
内存化优化
const paginationItems = React.useMemo(() => { return generatePaginationItems( currentPage, totalPages, siblingCount, boundaryCount )}, [currentPage, totalPages, siblingCount, boundaryCount])const contextValue = React.useMemo(() => ({ items: paginationItems, page: currentPage, totalPages, pageSize, onChange, getPaginationProps, getItemProps,}), [paginationItems, currentPage, totalPages, pageSize, onChange, getPaginationProps, getItemProps])
虚拟化渲染
对于超大数据集的分页,可以采用虚拟化渲染策略:
function VirtualizedPagination({ total, pageSize, ...props }) { const visibleItems = React.useMemo(() => { // 只渲染当前可见范围内的分页项 return generateVisiblePaginationItems(currentPage, totalPages) }, [currentPage, totalPages]) return ( ( )} {...props} /> )}
测试策略
单元测试示例
import { render, screen, fireEvent } from \'@testing-library/react\'import { usePagination } from \'./usePagination\'describe(\'usePagination\', () => { it(\'应该正确生成分页项\', () => { const TestComponent = () => { const { items } = usePagination({ total: 100, pageSize: 10 }) return ( {items.map((item, index) => ( {item.type} ))} ) } render() expect(screen.getByTestId(\'item-0\')).toHaveTextContent(\'page\') }) it(\'应该正确处理页码变化\', async () => { const onChange = jest.fn() const TestComponent = () => { const { getItemProps } = usePagination({ total: 100, pageSize: 10, onChange, }) return ( ) } render() fireEvent.click(screen.getByTestId(\'page-2\')) expect(onChange).toHaveBeenCalledWith(2, 10) })})
【免费下载链接】base-ui Base UI is a library of headless (\"unstyled\") React components and low-level hooks. You gain complete control over your app\'s CSS and accessibility features. 项目地址: https://gitcode.com/GitHub_Trending/ba/base-ui
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考