106 lines
4.7 KiB
TypeScript
106 lines
4.7 KiB
TypeScript
import { useState, type ReactNode } from "react"
|
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
|
import {
|
|
Pagination,
|
|
PaginationContent,
|
|
PaginationEllipsis,
|
|
PaginationItem,
|
|
PaginationLink,
|
|
PaginationNext,
|
|
PaginationPrevious,
|
|
} from "@hm-design/ui"
|
|
import { StateBoard } from "@/components/story-board"
|
|
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
|
|
|
// Pagination is presentational: the consumer owns the page and the hrefs. The Story keeps the page in local state and uses
|
|
// `#page-N` hrefs with preventDefault. The representative Story shows each case on one page; the same specimens also exist as
|
|
// Stories tagged `verification` (hidden from the sidebar).
|
|
|
|
// Page list with ellipses: 1 … (p-1 p p+1) … total.
|
|
function pageRange(page: number, total: number): (number | "start" | "end")[] {
|
|
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1)
|
|
const out: (number | "start" | "end")[] = [1]
|
|
const from = Math.max(2, Math.min(page - 1, total - 4))
|
|
const to = Math.min(total - 1, Math.max(page + 1, 5))
|
|
if (from > 2) out.push("start")
|
|
for (let p = from; p <= to; p++) out.push(p)
|
|
if (to < total - 1) out.push("end")
|
|
out.push(total)
|
|
return out
|
|
}
|
|
|
|
function Specimen({ total, initial }: { total: number; initial: number }) {
|
|
const [page, setPage] = useState(initial)
|
|
const go = (p: number) => (event: React.MouseEvent) => {
|
|
event.preventDefault()
|
|
setPage(p)
|
|
}
|
|
return (
|
|
<div className="grid gap-2">
|
|
<Pagination>
|
|
<PaginationContent>
|
|
<PaginationItem>
|
|
<PaginationPrevious href={`#page-${page - 1}`} aria-disabled={page === 1} onClick={go(page - 1)} />
|
|
</PaginationItem>
|
|
{pageRange(page, total).map((p) =>
|
|
typeof p === "number" ? (
|
|
<PaginationItem key={p}>
|
|
<PaginationLink href={`#page-${p}`} isActive={p === page} onClick={go(p)}>
|
|
{p}
|
|
</PaginationLink>
|
|
</PaginationItem>
|
|
) : (
|
|
<PaginationItem key={p}>
|
|
<PaginationEllipsis />
|
|
</PaginationItem>
|
|
)
|
|
)}
|
|
<PaginationItem>
|
|
<PaginationNext href={`#page-${page + 1}`} aria-disabled={page === total} onClick={go(page + 1)} />
|
|
</PaginationItem>
|
|
</PaginationContent>
|
|
</Pagination>
|
|
<p className="text-center text-sm text-muted-foreground" data-testid="page-state">{`현재 ${page} / ${total}`}</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const wrap = (node: ReactNode) => <div className="w-full max-w-xl">{node}</div>
|
|
|
|
const sections = [
|
|
{ id: "first", label: "First page", note: "Previous가 aria-disabled", node: <Specimen total={5} initial={1} /> },
|
|
{ id: "middle", label: "Middle page", node: <Specimen total={5} initial={3} /> },
|
|
{ id: "last", label: "Last page", note: "Next가 aria-disabled", node: <Specimen total={5} initial={5} /> },
|
|
{ id: "ellipsis", label: "Long range with ellipsis", note: "클릭하면 실제로 현재 페이지가 바뀝니다", node: <Specimen total={20} initial={10} /> },
|
|
]
|
|
|
|
const meta = { title: "Components/Navigation/Pagination", component: Pagination, parameters: { layout: "padded" } } satisfies Meta<typeof Pagination>
|
|
export default meta
|
|
type Story = StoryObj<typeof meta>
|
|
|
|
const board = (...ids: string[]) => (
|
|
<StateBoard min={420} states={sections.filter((s) => ids.includes(s.id)).map((s) => ({ id: s.id, label: s.label, note: s.note, children: wrap(s.node) }))} />
|
|
)
|
|
|
|
// Representative Story: the common Story page; the sections group the same specimens.
|
|
export const All: Story = {
|
|
name: "Pagination",
|
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
|
render: () => (
|
|
<StoryPage>
|
|
<StoryPageHeader
|
|
kicker={STORY_KICKER.components}
|
|
title="Pagination"
|
|
description="여러 페이지로 나뉜 목록을 이동하는 내비게이션입니다. 현재 페이지와 href는 소비 코드가 소유합니다."
|
|
/>
|
|
<StorySection title="Overview">{board("middle")}</StorySection>
|
|
<StorySection title="States" description="첫 페이지와 마지막 페이지에서는 이전·다음이 aria-disabled입니다.">{board("first", "last")}</StorySection>
|
|
<StorySection title="Usage" description="페이지가 많으면 생략 부호로 줄입니다.">{board("ellipsis")}</StorySection>
|
|
</StoryPage>
|
|
),
|
|
}
|
|
|
|
export const FirstPage: Story = { tags: ["verification"], name: "First Page", render: () => wrap(<Specimen total={5} initial={1} />) }
|
|
export const LastPage: Story = { tags: ["verification"], name: "Last Page", render: () => wrap(<Specimen total={5} initial={5} />) }
|
|
export const LongRange: Story = { tags: ["verification"], name: "Long Range", render: () => wrap(<Specimen total={20} initial={10} />) }
|