mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 22:04:56 +00:00
Merge pull request #189 from johndoe6345789/codex/refactor-pagination-components-and-utilities
Refactor pagination components into dedicated files
This commit is contained in:
@@ -74,7 +74,7 @@ export {
|
||||
PaginationEllipsis,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
} from './navigation/Pagination'
|
||||
} from './navigation/pagination'
|
||||
|
||||
// Navigation
|
||||
export {
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
NavigationLink,
|
||||
} from './NavigationMenuItems'
|
||||
import { NavigationBrand, NavigationSeparator, NavigationSpacer } from './NavigationStyling'
|
||||
import { NavigationItemType } from './navigationConfig'
|
||||
import { useNavigationDropdown } from './navigationHelpers'
|
||||
import { NavigationItemType } from './utils/navigationConfig'
|
||||
import { useNavigationDropdown } from './utils/navigationHelpers'
|
||||
|
||||
interface NavigationProps {
|
||||
children: ReactNode
|
||||
|
||||
@@ -1,406 +0,0 @@
|
||||
// TODO: Split this file (405 LOC) into smaller organisms (<150 LOC each)
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
import {
|
||||
Pagination as MuiPagination,
|
||||
PaginationItem,
|
||||
Box,
|
||||
IconButton,
|
||||
Typography,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
SelectChangeEvent,
|
||||
} from '@mui/material'
|
||||
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
|
||||
import FirstPageIcon from '@mui/icons-material/FirstPage'
|
||||
import LastPageIcon from '@mui/icons-material/LastPage'
|
||||
|
||||
// Pagination Root
|
||||
interface PaginationProps {
|
||||
count: number
|
||||
page: number
|
||||
onChange: (page: number) => void
|
||||
siblingCount?: number
|
||||
boundaryCount?: number
|
||||
showFirstButton?: boolean
|
||||
showLastButton?: boolean
|
||||
disabled?: boolean
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
variant?: 'text' | 'outlined'
|
||||
shape?: 'circular' | 'rounded'
|
||||
color?: 'primary' | 'secondary' | 'standard'
|
||||
}
|
||||
|
||||
const Pagination = forwardRef<HTMLElement, PaginationProps>(
|
||||
({
|
||||
count,
|
||||
page,
|
||||
onChange,
|
||||
siblingCount = 1,
|
||||
boundaryCount = 1,
|
||||
showFirstButton = false,
|
||||
showLastButton = false,
|
||||
disabled = false,
|
||||
size = 'medium',
|
||||
variant = 'outlined',
|
||||
shape = 'rounded',
|
||||
color = 'primary',
|
||||
...props
|
||||
}, ref) => {
|
||||
return (
|
||||
<MuiPagination
|
||||
ref={ref}
|
||||
count={count}
|
||||
page={page}
|
||||
onChange={(_, newPage) => onChange(newPage)}
|
||||
siblingCount={siblingCount}
|
||||
boundaryCount={boundaryCount}
|
||||
showFirstButton={showFirstButton}
|
||||
showLastButton={showLastButton}
|
||||
disabled={disabled}
|
||||
size={size}
|
||||
variant={variant}
|
||||
shape={shape}
|
||||
color={color}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Pagination.displayName = 'Pagination'
|
||||
|
||||
// Simplified Pagination (Previous/Next only)
|
||||
interface SimplePaginationProps {
|
||||
hasPrevious: boolean
|
||||
hasNext: boolean
|
||||
onPrevious: () => void
|
||||
onNext: () => void
|
||||
previousLabel?: string
|
||||
nextLabel?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const SimplePagination = forwardRef<HTMLDivElement, SimplePaginationProps>(
|
||||
({
|
||||
hasPrevious,
|
||||
hasNext,
|
||||
onPrevious,
|
||||
onNext,
|
||||
previousLabel = 'Previous',
|
||||
nextLabel = 'Next',
|
||||
disabled = false,
|
||||
...props
|
||||
}, ref) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 2,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<IconButton
|
||||
onClick={onPrevious}
|
||||
disabled={disabled || !hasPrevious}
|
||||
size="small"
|
||||
sx={{
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
borderRadius: 1,
|
||||
px: 2,
|
||||
py: 1,
|
||||
'&:hover': {
|
||||
bgcolor: 'action.hover',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ChevronLeftIcon fontSize="small" />
|
||||
<Typography variant="body2" sx={{ ml: 0.5 }}>
|
||||
{previousLabel}
|
||||
</Typography>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={onNext}
|
||||
disabled={disabled || !hasNext}
|
||||
size="small"
|
||||
sx={{
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
borderRadius: 1,
|
||||
px: 2,
|
||||
py: 1,
|
||||
'&:hover': {
|
||||
bgcolor: 'action.hover',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ mr: 0.5 }}>
|
||||
{nextLabel}
|
||||
</Typography>
|
||||
<ChevronRightIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
SimplePagination.displayName = 'SimplePagination'
|
||||
|
||||
// Table Pagination (with page size selector)
|
||||
interface TablePaginationProps {
|
||||
count: number
|
||||
page: number
|
||||
pageSize: number
|
||||
pageSizeOptions?: number[]
|
||||
onPageChange: (page: number) => void
|
||||
onPageSizeChange: (pageSize: number) => void
|
||||
showFirstLastButtons?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const TablePagination = forwardRef<HTMLDivElement, TablePaginationProps>(
|
||||
({
|
||||
count,
|
||||
page,
|
||||
pageSize,
|
||||
pageSizeOptions = [10, 25, 50, 100],
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
showFirstLastButtons = true,
|
||||
disabled = false,
|
||||
...props
|
||||
}, ref) => {
|
||||
const totalPages = Math.ceil(count / pageSize)
|
||||
const startItem = (page - 1) * pageSize + 1
|
||||
const endItem = Math.min(page * pageSize, count)
|
||||
|
||||
const handlePageSizeChange = (event: SelectChangeEvent<number>) => {
|
||||
onPageSizeChange(Number(event.target.value))
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
py: 1,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Rows per page:
|
||||
</Typography>
|
||||
<FormControl size="small" disabled={disabled}>
|
||||
<Select
|
||||
value={pageSize}
|
||||
onChange={handlePageSizeChange}
|
||||
sx={{ minWidth: 70 }}
|
||||
>
|
||||
{pageSizeOptions.map((option) => (
|
||||
<MenuItem key={option} value={option}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{count === 0 ? '0' : `${startItem}-${endItem}`} of {count}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
{showFirstLastButtons && (
|
||||
<IconButton
|
||||
onClick={() => onPageChange(1)}
|
||||
disabled={disabled || page === 1}
|
||||
size="small"
|
||||
>
|
||||
<FirstPageIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
<IconButton
|
||||
onClick={() => onPageChange(page - 1)}
|
||||
disabled={disabled || page === 1}
|
||||
size="small"
|
||||
>
|
||||
<ChevronLeftIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => onPageChange(page + 1)}
|
||||
disabled={disabled || page === totalPages}
|
||||
size="small"
|
||||
>
|
||||
<ChevronRightIcon fontSize="small" />
|
||||
</IconButton>
|
||||
{showFirstLastButtons && (
|
||||
<IconButton
|
||||
onClick={() => onPageChange(totalPages)}
|
||||
disabled={disabled || page === totalPages}
|
||||
size="small"
|
||||
>
|
||||
<LastPageIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
TablePagination.displayName = 'TablePagination'
|
||||
|
||||
// PaginationContent - wrapper for custom pagination content
|
||||
interface PaginationContentProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const PaginationContent = forwardRef<HTMLUListElement, PaginationContentProps>(
|
||||
({ children, ...props }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
component="ul"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
listStyle: 'none',
|
||||
p: 0,
|
||||
m: 0,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
PaginationContent.displayName = 'PaginationContent'
|
||||
|
||||
// PaginationItem wrapper
|
||||
interface PaginationItemWrapperProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const PaginationItemWrapper = forwardRef<HTMLLIElement, PaginationItemWrapperProps>(
|
||||
({ children, ...props }, ref) => {
|
||||
return (
|
||||
<Box component="li" ref={ref} {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
PaginationItemWrapper.displayName = 'PaginationItem'
|
||||
|
||||
// PaginationLink
|
||||
interface PaginationLinkProps {
|
||||
children: ReactNode
|
||||
onClick?: () => void
|
||||
isActive?: boolean
|
||||
disabled?: boolean
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
}
|
||||
|
||||
const PaginationLink = forwardRef<HTMLButtonElement, PaginationLinkProps>(
|
||||
({ children, onClick, isActive = false, disabled = false, size = 'medium', ...props }, ref) => {
|
||||
const sizeMap = {
|
||||
small: { minWidth: 28, height: 28 },
|
||||
medium: { minWidth: 36, height: 36 },
|
||||
large: { minWidth: 44, height: 44 },
|
||||
}
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
sx={{
|
||||
...sizeMap[size],
|
||||
borderRadius: 1,
|
||||
bgcolor: isActive ? 'primary.main' : 'transparent',
|
||||
color: isActive ? 'primary.contrastText' : 'text.primary',
|
||||
'&:hover': {
|
||||
bgcolor: isActive ? 'primary.dark' : 'action.hover',
|
||||
},
|
||||
'&.Mui-disabled': {
|
||||
opacity: 0.5,
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
)
|
||||
PaginationLink.displayName = 'PaginationLink'
|
||||
|
||||
// Pagination Ellipsis
|
||||
const PaginationEllipsis = forwardRef<HTMLSpanElement, Record<string, never>>(
|
||||
(props, ref) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
component="span"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 36,
|
||||
height: 36,
|
||||
color: 'text.secondary',
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
...
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
PaginationEllipsis.displayName = 'PaginationEllipsis'
|
||||
|
||||
// Previous/Next buttons
|
||||
const PaginationPrevious = forwardRef<HTMLButtonElement, Omit<PaginationLinkProps, 'children'>>(
|
||||
(props, ref) => {
|
||||
return (
|
||||
<PaginationLink ref={ref} {...props}>
|
||||
<ChevronLeftIcon fontSize="small" />
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
)
|
||||
PaginationPrevious.displayName = 'PaginationPrevious'
|
||||
|
||||
const PaginationNext = forwardRef<HTMLButtonElement, Omit<PaginationLinkProps, 'children'>>(
|
||||
(props, ref) => {
|
||||
return (
|
||||
<PaginationLink ref={ref} {...props}>
|
||||
<ChevronRightIcon fontSize="small" />
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
)
|
||||
PaginationNext.displayName = 'PaginationNext'
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
SimplePagination,
|
||||
TablePagination,
|
||||
PaginationContent,
|
||||
PaginationItemWrapper as PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationEllipsis,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationEllipsis,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
SimplePagination,
|
||||
TablePagination,
|
||||
} from './index'
|
||||
|
||||
describe('Pagination components', () => {
|
||||
it('triggers onChange for root Pagination', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(<Pagination count={3} page={1} onChange={handleChange} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Go to page 2' }))
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith(2)
|
||||
})
|
||||
|
||||
it('handles SimplePagination controls', () => {
|
||||
const onPrevious = vi.fn()
|
||||
const onNext = vi.fn()
|
||||
render(
|
||||
<SimplePagination
|
||||
hasPrevious={false}
|
||||
hasNext
|
||||
onPrevious={onPrevious}
|
||||
onNext={onNext}
|
||||
previousLabel="Prev"
|
||||
nextLabel="Next"
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByText('Prev').closest('button')?.disabled).toBe(true)
|
||||
fireEvent.click(screen.getByText('Next'))
|
||||
|
||||
expect(onNext).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('navigates TablePagination pages', () => {
|
||||
const onPageChange = vi.fn()
|
||||
const onPageSizeChange = vi.fn()
|
||||
render(
|
||||
<TablePagination
|
||||
count={30}
|
||||
page={2}
|
||||
pageSize={10}
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
showFirstLastButtons={false}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByText('11-20 of 30')).toBeDefined()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Go to next page' }))
|
||||
|
||||
expect(onPageChange).toHaveBeenCalledWith(3)
|
||||
})
|
||||
|
||||
it('renders content wrappers', () => {
|
||||
render(
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationLink isActive>1</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationEllipsis />
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
)
|
||||
|
||||
expect(screen.getByRole('list')).toBeDefined()
|
||||
expect(screen.getAllByRole('listitem')).to.have.length(2)
|
||||
})
|
||||
|
||||
it('renders link variants', () => {
|
||||
const handleClick = vi.fn()
|
||||
render(
|
||||
<PaginationLink onClick={handleClick} isActive>
|
||||
5
|
||||
</PaginationLink>
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
expect(handleClick).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('wraps icon links for previous and next', () => {
|
||||
const onPrevious = vi.fn()
|
||||
const onNext = vi.fn()
|
||||
render(
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious onClick={onPrevious} />
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext onClick={onNext} />
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
)
|
||||
|
||||
const buttons = screen.getAllByRole('button')
|
||||
fireEvent.click(buttons[0])
|
||||
fireEvent.click(buttons[1])
|
||||
|
||||
expect(onPrevious).toHaveBeenCalled()
|
||||
expect(onNext).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders ellipsis marker', () => {
|
||||
render(<PaginationEllipsis />)
|
||||
|
||||
expect(screen.getByText('...')).toBeDefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, ReactNode } from 'react'
|
||||
import { Box } from '@mui/material'
|
||||
|
||||
interface PaginationContentProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
interface PaginationItemWrapperProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const PaginationContent = forwardRef<HTMLUListElement, PaginationContentProps>(
|
||||
({ children, ...props }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
component="ul"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
listStyle: 'none',
|
||||
p: 0,
|
||||
m: 0,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
PaginationContent.displayName = 'PaginationContent'
|
||||
|
||||
const PaginationItem = forwardRef<HTMLLIElement, PaginationItemWrapperProps>(
|
||||
({ children, ...props }, ref) => {
|
||||
return (
|
||||
<Box component="li" ref={ref} {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
PaginationItem.displayName = 'PaginationItem'
|
||||
|
||||
export { PaginationContent, PaginationItem }
|
||||
export type { PaginationContentProps, PaginationItemWrapperProps }
|
||||
@@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
import { Box } from '@mui/material'
|
||||
|
||||
const PaginationEllipsis = forwardRef<HTMLSpanElement, Record<string, never>>((props, ref) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
component="span"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 36,
|
||||
height: 36,
|
||||
color: 'text.secondary',
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
...
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
PaginationEllipsis.displayName = 'PaginationEllipsis'
|
||||
|
||||
export { PaginationEllipsis }
|
||||
@@ -0,0 +1,35 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
import { IconButton } from '@mui/material'
|
||||
import { paginationSizeMap, type PaginationLinkProps } from './paginationUtils'
|
||||
|
||||
const PaginationLink = forwardRef<HTMLButtonElement, PaginationLinkProps>(
|
||||
({ children, onClick, isActive = false, disabled = false, size = 'medium', ...props }, ref) => {
|
||||
return (
|
||||
<IconButton
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
sx={{
|
||||
...paginationSizeMap[size],
|
||||
borderRadius: 1,
|
||||
bgcolor: isActive ? 'primary.main' : 'transparent',
|
||||
color: isActive ? 'primary.contrastText' : 'text.primary',
|
||||
'&:hover': {
|
||||
bgcolor: isActive ? 'primary.dark' : 'action.hover',
|
||||
},
|
||||
'&.Mui-disabled': {
|
||||
opacity: 0.5,
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
)
|
||||
PaginationLink.displayName = 'PaginationLink'
|
||||
|
||||
export { PaginationLink }
|
||||
@@ -0,0 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
import { PaginationLink } from './PaginationLink'
|
||||
import { NextIcon, type PaginationLinkProps } from './paginationUtils'
|
||||
|
||||
const PaginationNext = forwardRef<HTMLButtonElement, Omit<PaginationLinkProps, 'children'>>(
|
||||
(props, ref) => {
|
||||
return (
|
||||
<PaginationLink ref={ref} {...props}>
|
||||
<NextIcon />
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
)
|
||||
PaginationNext.displayName = 'PaginationNext'
|
||||
|
||||
export { PaginationNext }
|
||||
@@ -0,0 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
import { PaginationLink } from './PaginationLink'
|
||||
import { PreviousIcon, type PaginationLinkProps } from './paginationUtils'
|
||||
|
||||
const PaginationPrevious = forwardRef<HTMLButtonElement, Omit<PaginationLinkProps, 'children'>>(
|
||||
(props, ref) => {
|
||||
return (
|
||||
<PaginationLink ref={ref} {...props}>
|
||||
<PreviousIcon />
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
)
|
||||
PaginationPrevious.displayName = 'PaginationPrevious'
|
||||
|
||||
export { PaginationPrevious }
|
||||
@@ -0,0 +1,63 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
import { Pagination as MuiPagination } from '@mui/material'
|
||||
|
||||
interface PaginationProps {
|
||||
count: number
|
||||
page: number
|
||||
onChange: (page: number) => void
|
||||
siblingCount?: number
|
||||
boundaryCount?: number
|
||||
showFirstButton?: boolean
|
||||
showLastButton?: boolean
|
||||
disabled?: boolean
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
variant?: 'text' | 'outlined'
|
||||
shape?: 'circular' | 'rounded'
|
||||
color?: 'primary' | 'secondary' | 'standard'
|
||||
}
|
||||
|
||||
const Pagination = forwardRef<HTMLElement, PaginationProps>(
|
||||
(
|
||||
{
|
||||
count,
|
||||
page,
|
||||
onChange,
|
||||
siblingCount = 1,
|
||||
boundaryCount = 1,
|
||||
showFirstButton = false,
|
||||
showLastButton = false,
|
||||
disabled = false,
|
||||
size = 'medium',
|
||||
variant = 'outlined',
|
||||
shape = 'rounded',
|
||||
color = 'primary',
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<MuiPagination
|
||||
ref={ref}
|
||||
count={count}
|
||||
page={page}
|
||||
onChange={(_, newPage) => onChange(newPage)}
|
||||
siblingCount={siblingCount}
|
||||
boundaryCount={boundaryCount}
|
||||
showFirstButton={showFirstButton}
|
||||
showLastButton={showLastButton}
|
||||
disabled={disabled}
|
||||
size={size}
|
||||
variant={variant}
|
||||
shape={shape}
|
||||
color={color}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Pagination.displayName = 'Pagination'
|
||||
|
||||
export { Pagination }
|
||||
export type { PaginationProps }
|
||||
@@ -0,0 +1,89 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
import { Box, IconButton, Typography } from '@mui/material'
|
||||
import { NextIcon, PreviousIcon } from './paginationUtils'
|
||||
|
||||
interface SimplePaginationProps {
|
||||
hasPrevious: boolean
|
||||
hasNext: boolean
|
||||
onPrevious: () => void
|
||||
onNext: () => void
|
||||
previousLabel?: string
|
||||
nextLabel?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const SimplePagination = forwardRef<HTMLDivElement, SimplePaginationProps>(
|
||||
(
|
||||
{
|
||||
hasPrevious,
|
||||
hasNext,
|
||||
onPrevious,
|
||||
onNext,
|
||||
previousLabel = 'Previous',
|
||||
nextLabel = 'Next',
|
||||
disabled = false,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 2,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<IconButton
|
||||
onClick={onPrevious}
|
||||
disabled={disabled || !hasPrevious}
|
||||
size="small"
|
||||
sx={{
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
borderRadius: 1,
|
||||
px: 2,
|
||||
py: 1,
|
||||
'&:hover': {
|
||||
bgcolor: 'action.hover',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<PreviousIcon />
|
||||
<Typography variant="body2" sx={{ ml: 0.5 }}>
|
||||
{previousLabel}
|
||||
</Typography>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={onNext}
|
||||
disabled={disabled || !hasNext}
|
||||
size="small"
|
||||
sx={{
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
borderRadius: 1,
|
||||
px: 2,
|
||||
py: 1,
|
||||
'&:hover': {
|
||||
bgcolor: 'action.hover',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ mr: 0.5 }}>
|
||||
{nextLabel}
|
||||
</Typography>
|
||||
<NextIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
SimplePagination.displayName = 'SimplePagination'
|
||||
|
||||
export { SimplePagination }
|
||||
export type { SimplePaginationProps }
|
||||
@@ -0,0 +1,128 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
import {
|
||||
Box,
|
||||
IconButton,
|
||||
Typography,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
SelectChangeEvent,
|
||||
} from '@mui/material'
|
||||
import FirstPageIcon from '@mui/icons-material/FirstPage'
|
||||
import LastPageIcon from '@mui/icons-material/LastPage'
|
||||
import { NextIcon, PreviousIcon } from './paginationUtils'
|
||||
|
||||
interface TablePaginationProps {
|
||||
count: number
|
||||
page: number
|
||||
pageSize: number
|
||||
pageSizeOptions?: number[]
|
||||
onPageChange: (page: number) => void
|
||||
onPageSizeChange: (pageSize: number) => void
|
||||
showFirstLastButtons?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const TablePagination = forwardRef<HTMLDivElement, TablePaginationProps>(
|
||||
(
|
||||
{
|
||||
count,
|
||||
page,
|
||||
pageSize,
|
||||
pageSizeOptions = [10, 25, 50, 100],
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
showFirstLastButtons = true,
|
||||
disabled = false,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const totalPages = Math.ceil(count / pageSize)
|
||||
const startItem = (page - 1) * pageSize + 1
|
||||
const endItem = Math.min(page * pageSize, count)
|
||||
|
||||
const handlePageSizeChange = (event: SelectChangeEvent<number>) => {
|
||||
onPageSizeChange(Number(event.target.value))
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
py: 1,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Rows per page:
|
||||
</Typography>
|
||||
<FormControl size="small" disabled={disabled}>
|
||||
<Select value={pageSize} onChange={handlePageSizeChange} sx={{ minWidth: 70 }}>
|
||||
{pageSizeOptions.map((option) => (
|
||||
<MenuItem key={option} value={option}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{count === 0 ? '0' : `${startItem}-${endItem}`} of {count}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
{showFirstLastButtons && (
|
||||
<IconButton
|
||||
onClick={() => onPageChange(1)}
|
||||
disabled={disabled || page === 1}
|
||||
size="small"
|
||||
aria-label="Go to first page"
|
||||
>
|
||||
<FirstPageIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
<IconButton
|
||||
onClick={() => onPageChange(page - 1)}
|
||||
disabled={disabled || page === 1}
|
||||
size="small"
|
||||
aria-label="Go to previous page"
|
||||
>
|
||||
<PreviousIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => onPageChange(page + 1)}
|
||||
disabled={disabled || page === totalPages}
|
||||
size="small"
|
||||
aria-label="Go to next page"
|
||||
>
|
||||
<NextIcon />
|
||||
</IconButton>
|
||||
{showFirstLastButtons && (
|
||||
<IconButton
|
||||
onClick={() => onPageChange(totalPages)}
|
||||
disabled={disabled || page === totalPages}
|
||||
size="small"
|
||||
aria-label="Go to last page"
|
||||
>
|
||||
<LastPageIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
TablePagination.displayName = 'TablePagination'
|
||||
|
||||
export { TablePagination }
|
||||
export type { TablePaginationProps }
|
||||
@@ -0,0 +1,13 @@
|
||||
export { Pagination } from './PaginationRoot'
|
||||
export type { PaginationProps } from './PaginationRoot'
|
||||
export { SimplePagination } from './SimplePagination'
|
||||
export type { SimplePaginationProps } from './SimplePagination'
|
||||
export { TablePagination } from './TablePagination'
|
||||
export type { TablePaginationProps } from './TablePagination'
|
||||
export { PaginationContent, PaginationItem } from './PaginationContent'
|
||||
export type { PaginationContentProps, PaginationItemWrapperProps } from './PaginationContent'
|
||||
export { PaginationLink } from './PaginationLink'
|
||||
export { PaginationEllipsis } from './PaginationEllipsis'
|
||||
export { PaginationPrevious } from './PaginationPrevious'
|
||||
export { PaginationNext } from './PaginationNext'
|
||||
export type { PaginationLinkProps } from './paginationUtils'
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ReactNode } from 'react'
|
||||
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
|
||||
|
||||
interface PaginationLinkProps {
|
||||
children: ReactNode
|
||||
onClick?: () => void
|
||||
isActive?: boolean
|
||||
disabled?: boolean
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
}
|
||||
|
||||
const paginationSizeMap = {
|
||||
small: { minWidth: 28, height: 28 },
|
||||
medium: { minWidth: 36, height: 36 },
|
||||
large: { minWidth: 44, height: 44 },
|
||||
}
|
||||
|
||||
const PreviousIcon = () => <ChevronLeftIcon fontSize="small" />
|
||||
|
||||
const NextIcon = () => <ChevronRightIcon fontSize="small" />
|
||||
|
||||
export { paginationSizeMap, PreviousIcon, NextIcon }
|
||||
export type { PaginationLinkProps }
|
||||
Reference in New Issue
Block a user