التنقّل بين الصفحات (Pagination)

صفّ من روابط الصفحات للتنقّل في قائمة طويلة أو بين نتائج البحث. روابط الصفحات وزرّا السابق والتالي كلها مبنية على مكوّن زر، ويأتي المكوّن بثلاثة أنماط وشكلين للزوايا وأربعة أحجام، كما في Figma.

التثبيت

bash
npx @smicolon/fasla-ui@latest add pagination

قبل إضافة أي مكوّن، شغّل الأمر init مرة واحدة في مشروعك. التفاصيل في صفحة التثبيت.

المكوّن مبني على مكوّن زر، فإضافته تضيف معه العنصر button من السجل.

معاينة

الأنماط (variant)

تختار الخاصية variant نمط الزر لكل جزء. في النمط default تكون روابط الصفحات بنمط Ghost والصفحة الحالية بنمط Solid. وفي النمط solid تكون روابط الصفحات بنمط Soft والصفحة الحالية بنمط Solid. أما في النمط outlined فروابط الصفحات وزرّا السابق والتالي بنمط Outline، والصفحة الحالية بنمط Soft. وفي النمطين الآخرين يكون زرّا السابق والتالي بنمط Ghost.

الأحجام

أحجام مكوّن زر الأربعة: ارتفاع الحجم xs هو 26px، والحجم sm هو 32px، والحجم md هو 36px، والحجم lg هو 40px. ورابط الصفحة مربّع بهذا الارتفاع، ورقمه بحجم 14px في كل الأحجام. وإذا لم يتّسع المربّع لرقم، مثل 100 في الحجم xs، اتّسع المربّع له.

الشكل

تعطي القيمة square للخاصية shape كل الأجزاء زوايا الزر القياسية بنصف قطر 8px، وتعطيها القيمة rounded أقصى استدارة: دوائر لروابط الصفحات، وشكل الكبسولة لزرّي السابق والتالي. وتتبع حلقة التركيز شكل الجزء.

زرّا السابق والتالي

يعرض المكوّنان PaginationPrevious، PaginationNext تسمية وسهمًا. وتتبع التسمية لغة الصفحة، فهي «السابق» و«التالي» في الصفحة العربية، ولتغييرها استخدم الخاصية text. ومع الخاصية iconOnly يظهر السهم وحده في مربّع، وتبقى التسمية لقارئ الشاشة. وعطّل زر السابق في الصفحة الأولى، وزر التالي في الأخيرة، بالخاصية disabled.

النطاقات الطويلة

اعرض الصفحة الأولى والأخيرة، والصفحة الحالية مع صفحة على كل جانب منها، وضع المكوّن PaginationEllipsis مكان الصفحات المحذوفة: 1 … 5 6 7 … 20. والنقاط الثلاث مربّع بحجم رابط الصفحة، لكنها ليست رابطًا، ويسمع منها قارئ الشاشة «مزيد من الصفحات». ويُبقي زرّا السابق والتالي بالسهم وحده الصفَّ الطويل قصيرًا. وعلى شاشة الهاتف اعرض صفحات أقل حول الصفحة الحالية، كما يفعل هذا المثال.

إتاحة الوصول (accessibility)

  • المكوّن Pagination منطقة تنقّل في الصفحة. سمّها بلغة الصفحة بالسمة aria-label كما تفعل الأمثلة هنا، وإلا فاسمها Pagination. وأعطِ كل منطقة منها في الصفحة اسمًا خاصًا بها.
  • تحمل الصفحة الحالية السمة aria-current="page"، فيعلن قارئ الشاشة أنها الصفحة الحالية.
  • اسم زرّي السابق والتالي هو تسميتهما، وتبقى مقروءة حين تخفيها الخاصية iconOnly عن العين. والأسهم مخفية عن التقنيات المساعدة.
  • لا تأخذ النقاط الثلاث التركيز. رمزها مخفي، ويقرؤها قارئ الشاشة «مزيد من الصفحات»، أو «More pages» حين تكون أقرب قيمة لـ lang هي الإنجليزية.
  • يحمل الرابط المعطّل السمة aria-disabled، ويخرج من ترتيب التنقّل بمفتاح Tab، ولا يستجيب للنقر. ويبقى رابطًا، فيجده قارئ الشاشة.
  • كل جزء زر من مكوّن زر، فيرسم التركيز من لوحة المفاتيح حلقة الزر بعرض 2px، على بعد 2px من الرابط.
  • يتبع الاتجاه السمة dir في الصفحة، ولا توجد له خاصية. في الصفحات من اليمين إلى اليسار يبدأ الصفّ من اليمين بزر السابق، ويشير كل سهم إلى الخارج. وتبقى أرقام الصفحات 1، 2، 3 لا ١، ٢، ٣.

الخصائص (props)

Pagination

الخاصيةالنوعالافتراضيالوصف
variant"default" | "solid" | "outlined""default"نمط الزر لكل جزء: روابط صفحات بنمط Ghost أو Soft أو Outline.
shape"square" | "rounded""square"زوايا كل الأجزاء: زوايا الزر القياسية، أو أقصى استدارة.
size"xs" | "sm" | "md" | "lg""md"ارتفاع كل الأجزاء، من 26px إلى 40px.
aria-labelstring"Pagination"اسم منطقة التنقّل لقارئ الشاشة، بلغة الصفحة.
childrenReactNodeلا يوجدالمكوّن PaginationContent.
classNamestringلا يوجدأصناف إضافية للعنصر nav، وهو افتراضيًا صفّ في المنتصف.

PaginationContent

الخاصيةالنوعالافتراضيالوصف
childrenReactNodeلا يوجدعناصر PaginationItem بترتيب القراءة.
classNamestringلا يوجدأصناف إضافية للعنصر ul. وينتقل الصفّ إلى سطر ثانٍ بدل أن يتجاوز حاويته.

PaginationItem

الخاصيةالنوعالافتراضيالوصف
childrenReactNodeلا يوجدرابط صفحة واحد، أو زر السابق أو التالي، أو النقاط الثلاث.
classNamestringلا يوجدأصناف إضافية للعنصر li.

PaginationLink

الخاصيةالنوعالافتراضيالوصف
hrefstringلا يوجدوجهة الرابط. ويقبل الرابط كل سمات العنصر a الأخرى أيضًا.
isActivebooleanfalseيعلّم الصفحة الحالية: بمظهر الصفحة الحالية، وبالسمة aria-current="page".
disabledbooleanfalseيُخفت الرابط، ويُخرجه من ترتيب التنقّل بمفتاح Tab، ويتجاهل النقر.
asChildbooleanfalseيجعل عنصره الابن الوحيد هو الرابط بهذا الشكل، مثل المكوّن Link في Next.js، أو العنصر button إذا كان التنقّل يغيّر الحالة لا عنوان الصفحة.
childrenReactNodeلا يوجدرقم الصفحة، يُكتب 1، 2، 3 في الصفحات العربية أيضًا.
classNamestringلا يوجدأصناف إضافية للرابط.

PaginationPrevious, PaginationNext

الخاصيةالنوعالافتراضيالوصف
hrefstringلا يوجدوجهة الرابط.
textReactNodeلا يوجدالتسمية. افتراضيًا «السابق» أو «التالي» بلغة الصفحة.
iconOnlybooleanfalseيُظهر السهم وحده في مربّع، وتبقى التسمية لقارئ الشاشة.
disabledbooleanfalseيُخفت الرابط، ويُخرجه من ترتيب التنقّل بمفتاح Tab، ويتجاهل النقر.
asChildbooleanfalseيجعل عنصره الابن الوحيد هو الرابط، ويضع فيه التسمية والسهم.
classNamestringلا يوجدأصناف إضافية للرابط.

PaginationEllipsis

الخاصيةالنوعالافتراضيالوصف
classNamestringلا يوجدأصناف إضافية للنقاط الثلاث. نص قارئ الشاشة مضمَّن فيها.

الاستخدام

روابط إلى الصفحات

tsx
import {
  Pagination,
  PaginationContent,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"

export function OrdersPagination() {
  return (
    <Pagination aria-label="التنقّل بين الصفحات">
      <PaginationContent>
        <PaginationItem>
          <PaginationPrevious href="?page=1" />
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="?page=1">1</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="?page=2" isActive>
            2
          </PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="?page=3">3</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationNext href="?page=3" />
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}

نطاق طويل

tsx
import {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"

const PAGES = [1, null, 5, 6, 7, null, 20]

export function ArticlesPagination() {
  return (
    <Pagination aria-label="التنقّل بين الصفحات" variant="outlined">
      <PaginationContent>
        <PaginationItem>
          <PaginationPrevious href="?page=5" iconOnly />
        </PaginationItem>
        {PAGES.map((page, i) => (
          <PaginationItem key={i}>
            {page === null ? (
              <PaginationEllipsis />
            ) : (
              <PaginationLink href={`?page=${page}`} isActive={page === 6}>
                {page}
              </PaginationLink>
            )}
          </PaginationItem>
        ))}
        <PaginationItem>
          <PaginationNext href="?page=7" iconOnly />
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}

التنقّل بالحالة

tsx
"use client"

import { useState } from "react"
import {
  Pagination,
  PaginationContent,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"

const TOTAL = 5

export function TablePagination() {
  const [page, setPage] = useState(1)

  return (
    <Pagination aria-label="التنقّل بين الصفحات" size="sm">
      <PaginationContent>
        <PaginationItem>
          <PaginationPrevious asChild disabled={page === 1}>
            <button type="button" onClick={() => setPage(page - 1)} />
          </PaginationPrevious>
        </PaginationItem>
        {Array.from({ length: TOTAL }, (_, i) => i + 1).map((n) => (
          <PaginationItem key={n}>
            <PaginationLink asChild isActive={n === page}>
              <button type="button" onClick={() => setPage(n)}>
                {n}
              </button>
            </PaginationLink>
          </PaginationItem>
        ))}
        <PaginationItem>
          <PaginationNext asChild disabled={page === TOTAL}>
            <button type="button" onClick={() => setPage(page + 1)} />
          </PaginationNext>
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}

مع المكوّن Link في Next.js

tsx
import Link from "next/link"
import {
  Pagination,
  PaginationContent,
  PaginationItem,
  PaginationLink,
  PaginationNext,
} from "@/components/ui/pagination"

export function ResultsPagination() {
  return (
    <Pagination aria-label="التنقّل بين الصفحات" shape="rounded">
      <PaginationContent>
        <PaginationItem>
          <PaginationLink asChild isActive>
            <Link href="/results?page=1">1</Link>
          </PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink asChild>
            <Link href="/results?page=2">2</Link>
          </PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationNext asChild>
            <Link href="/results?page=2" />
          </PaginationNext>
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}