Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/components/src/components/Pagination/IconChevronLeft.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export function IconChevronLeft({ className }: { className?: string }) {
return (
<svg
className={className}
fill="none"
height="16"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 12L6 8L10 4"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
/>
</svg>
)
}
20 changes: 20 additions & 0 deletions packages/components/src/components/Pagination/IconChevronRight.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export function IconChevronRight({ className }: { className?: string }) {
return (
<svg
className={className}
fill="none"
height="16"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 4L10 8L6 12"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
/>
</svg>
)
}
119 changes: 119 additions & 0 deletions packages/components/src/components/Pagination/Pagination.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Meta, StoryObj } from '@storybook/react-vite'
import { useState } from 'react'

import {
Pagination,
PaginationContainer,
PaginationNextButton,
PaginationPages,
PaginationPrevButton,
} from './index'

type Story = StoryObj<typeof meta>

const meta: Meta<typeof Pagination> = {
title: 'Devfive/Pagination',
component: Pagination,
decorators: [
(Story) => (
<div style={{ padding: '10px' }}>
<Story />
</div>
),
],
}

export const Default: Story = {
args: {
totalPages: 10,
defaultPage: 1,
},
}

export const PropsBasedSimple: Story = {
args: {
totalPages: 10,
defaultPage: 1,
},
render: (args) => <Pagination {...args} />,
}

export const WithoutPrevNext: Story = {
args: {
totalPages: 10,
defaultPage: 1,
showPrevNext: false,
},
render: (args) => <Pagination {...args} />,
}

export const CompositionBased: Story = {
args: {
totalPages: 10,
defaultPage: 1,
},
render: (args) => (
<Pagination {...args}>
<PaginationContainer>
<PaginationPrevButton />
<PaginationPages />
<PaginationNextButton />
</PaginationContainer>
</Pagination>
),
}

export const Controlled: Story = {
render: () => {
const [currentPage, setCurrentPage] = useState(1)

Check failure on line 68 in packages/components/src/components/Pagination/Pagination.stories.tsx

View workflow job for this annotation

GitHub Actions / publish

React Hook "useState" is called in function "render" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use"

return (
<div>
<div style={{ marginBottom: '10px' }}>Current Page: {currentPage}</div>
<Pagination
currentPage={currentPage}
onPageChange={setCurrentPage}
totalPages={15}
/>
</div>
)
},
}

export const ManyPages: Story = {
args: {
totalPages: 100,
defaultPage: 50,
},
}

export const FewPages: Story = {
args: {
totalPages: 5,
defaultPage: 1,
},
}

export const WithoutFirstLast: Story = {
args: {
totalPages: 20,
defaultPage: 10,
showFirstLast: false,
},
}

export const CustomClassName: Story = {
args: {
totalPages: 10,
defaultPage: 1,
className: 'custom-pagination',
},
render: (args) => (
<div>
<style>{`.custom-pagination { padding: 10px; background: rgba(129, 99, 225, 0.1); border-radius: 8px; }`}</style>
<Pagination {...args} />
</div>
),
}

export default meta
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import { fireEvent, render } from '@testing-library/react'

import {
Pagination,
PaginationContainer,
PaginationNextButton,
PaginationPageButton,
PaginationPages,
PaginationPrevButton,
} from '..'

describe('Pagination', () => {
it('should render with composition pattern', () => {
const { container } = render(
<Pagination totalPages={10}>
<PaginationContainer>
<PaginationPrevButton />
<PaginationPages />
<PaginationNextButton />
</PaginationContainer>
</Pagination>,
)
expect(container).toMatchSnapshot()
})

it('should render with props-based pattern', () => {
const { container } = render(<Pagination totalPages={10} />)
expect(container).toMatchSnapshot()
expect(container.querySelector('[aria-label="Previous page"]')).toBeInTheDocument()

Check failure on line 29 in packages/components/src/components/Pagination/__tests__/index.browser.test.tsx

View workflow job for this annotation

GitHub Actions / publish

Replace `container.querySelector('[aria-label="Previous·page"]')` with `⏎······container.querySelector('[aria-label="Previous·page"]'),⏎····`
expect(container.querySelector('[aria-label="Next page"]')).toBeInTheDocument()

Check failure on line 30 in packages/components/src/components/Pagination/__tests__/index.browser.test.tsx

View workflow job for this annotation

GitHub Actions / publish

Replace `container.querySelector('[aria-label="Next·page"]')` with `⏎······container.querySelector('[aria-label="Next·page"]'),⏎····`
expect(container.querySelector('[aria-label="Page 1"]')).toBeInTheDocument()
})

it('should render without prev/next buttons when showPrevNext is false', () => {
const { container } = render(<Pagination showPrevNext={false} totalPages={10} />)

Check failure on line 35 in packages/components/src/components/Pagination/__tests__/index.browser.test.tsx

View workflow job for this annotation

GitHub Actions / publish

Replace `<Pagination·showPrevNext={false}·totalPages={10}·/>` with `⏎······<Pagination·showPrevNext={false}·totalPages={10}·/>,⏎····`
expect(container.querySelector('[aria-label="Previous page"]')).not.toBeInTheDocument()

Check failure on line 36 in packages/components/src/components/Pagination/__tests__/index.browser.test.tsx

View workflow job for this annotation

GitHub Actions / publish

Replace `container.querySelector('[aria-label="Previous·page"]')` with `⏎······container.querySelector('[aria-label="Previous·page"]'),⏎····`
expect(container.querySelector('[aria-label="Next page"]')).not.toBeInTheDocument()

Check failure on line 37 in packages/components/src/components/Pagination/__tests__/index.browser.test.tsx

View workflow job for this annotation

GitHub Actions / publish

Replace `container.querySelector('[aria-label="Next·page"]')` with `⏎······container.querySelector('[aria-label="Next·page"]'),⏎····`
expect(container.querySelector('[aria-label="Page 1"]')).toBeInTheDocument()
})

it('should apply custom className to container', () => {
const { container } = render(<Pagination className="custom-class" totalPages={10} />)

Check failure on line 42 in packages/components/src/components/Pagination/__tests__/index.browser.test.tsx

View workflow job for this annotation

GitHub Actions / publish

Replace `<Pagination·className="custom-class"·totalPages={10}·/>` with `⏎······<Pagination·className="custom-class"·totalPages={10}·/>,⏎····`
expect(container.querySelector('.custom-class')).toBeInTheDocument()
})

it('should throw error if children are used outside of Pagination', () => {
expect(() => {
render(<PaginationPrevButton />)
}).toThrow('usePagination must be used within a Pagination component')
})

it('should call onPageChange when page is changed', () => {
const onPageChange = vi.fn()
const { container } = render(
<Pagination onPageChange={onPageChange} totalPages={10}>
<PaginationContainer>
<PaginationPageButton page={2} />
</PaginationContainer>
</Pagination>,
)
const button = container.querySelector('[aria-label="Page 2"]')
fireEvent.click(button!)
expect(onPageChange).toHaveBeenCalledWith(2)
})

it('should change internal page when onPageChange is not provided', () => {
const { container } = render(
<Pagination totalPages={10}>
<PaginationContainer>
<PaginationPageButton page={2} />
</PaginationContainer>
</Pagination>,
)
const button = container.querySelector('[aria-label="Page 2"]')
fireEvent.click(button!)
expect(button).toHaveAttribute('aria-current', 'page')
})

it('should disable prev button when on first page', () => {
const { container } = render(
<Pagination defaultPage={1} totalPages={10}>
<PaginationContainer>
<PaginationPrevButton />
</PaginationContainer>
</Pagination>,
)
const prevButton = container.querySelector('[aria-label="Previous page"]')
expect(prevButton).toHaveAttribute('disabled')
})

it('should disable next button when on last page', () => {
const { container } = render(
<Pagination defaultPage={10} totalPages={10}>
<PaginationContainer>
<PaginationNextButton />
</PaginationContainer>
</Pagination>,
)
const nextButton = container.querySelector('[aria-label="Next page"]')
expect(nextButton).toHaveAttribute('disabled')
})

it('should navigate to next page when next button is clicked', () => {
const onPageChange = vi.fn()
const { container } = render(
<Pagination

Check failure on line 106 in packages/components/src/components/Pagination/__tests__/index.browser.test.tsx

View workflow job for this annotation

GitHub Actions / publish

Replace `⏎········currentPage={1}⏎········onPageChange={onPageChange}⏎········totalPages={10}⏎······` with `·currentPage={1}·onPageChange={onPageChange}·totalPages={10}`
currentPage={1}
onPageChange={onPageChange}
totalPages={10}
>
<PaginationContainer>
<PaginationNextButton />
</PaginationContainer>
</Pagination>,
)
const nextButton = container.querySelector('[aria-label="Next page"]')
fireEvent.click(nextButton!)
expect(onPageChange).toHaveBeenCalledWith(2)
})

it('should navigate to previous page when prev button is clicked', () => {
const onPageChange = vi.fn()
const { container } = render(
<Pagination

Check failure on line 124 in packages/components/src/components/Pagination/__tests__/index.browser.test.tsx

View workflow job for this annotation

GitHub Actions / publish

Replace `⏎········currentPage={5}⏎········onPageChange={onPageChange}⏎········totalPages={10}⏎······` with `·currentPage={5}·onPageChange={onPageChange}·totalPages={10}`
currentPage={5}
onPageChange={onPageChange}
totalPages={10}
>
<PaginationContainer>
<PaginationPrevButton />
</PaginationContainer>
</Pagination>,
)
const prevButton = container.querySelector('[aria-label="Previous page"]')
fireEvent.click(prevButton!)
expect(onPageChange).toHaveBeenCalledWith(4)
})

it('should render all pages when total is 7 or less', () => {
const { container } = render(
<Pagination totalPages={5}>
<PaginationPages />
</Pagination>,
)
const buttons = container.querySelectorAll('[aria-label^="Page"]')
expect(buttons.length).toBe(5)
})

it('should render ellipsis for many pages', () => {
const { container } = render(
<Pagination defaultPage={1} totalPages={20}>
<PaginationPages />
</Pagination>,
)
const ellipsis = container.querySelector('[aria-hidden="true"]')
expect(ellipsis).toBeInTheDocument()
expect(ellipsis?.textContent).toBe('...')
})

it('should mark active page button', () => {
const { container } = render(
<Pagination currentPage={3} totalPages={10}>
<PaginationPageButton page={3} />
</Pagination>,
)
const button = container.querySelector('[aria-label="Page 3"]')
expect(button).toHaveAttribute('aria-current', 'page')
})

it('should export components', async () => {
const index = await import('../index')
expect({ ...index }).toEqual({
Pagination: expect.any(Function),
PaginationContainer: expect.any(Function),
PaginationEllipsis: expect.any(Function),
PaginationNextButton: expect.any(Function),
PaginationPageButton: expect.any(Function),
PaginationPages: expect.any(Function),
PaginationPrevButton: expect.any(Function),
usePagination: expect.any(Function),
})
})

it('should not exceed totalPages when navigating', () => {
const onPageChange = vi.fn()
const { container } = render(
<Pagination

Check failure on line 187 in packages/components/src/components/Pagination/__tests__/index.browser.test.tsx

View workflow job for this annotation

GitHub Actions / publish

Replace `⏎········currentPage={10}⏎········onPageChange={onPageChange}⏎········totalPages={10}⏎······` with `·currentPage={10}·onPageChange={onPageChange}·totalPages={10}`
currentPage={10}
onPageChange={onPageChange}
totalPages={10}
>
<PaginationContainer>
<PaginationNextButton />
</PaginationContainer>
</Pagination>,
)
const nextButton = container.querySelector('[aria-label="Next page"]')
expect(nextButton).toHaveAttribute('disabled')
})

it('should not go below 1 when navigating', () => {
const onPageChange = vi.fn()
const { container } = render(
<Pagination
currentPage={1}
onPageChange={onPageChange}
totalPages={10}
>
<PaginationContainer>
<PaginationPrevButton />
</PaginationContainer>
</Pagination>,
)
const prevButton = container.querySelector('[aria-label="Previous page"]')
expect(prevButton).toHaveAttribute('disabled')
})
})
Loading