snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
443
apps/v4/examples/aria/data-table-rtl.tsx
Normal file
443
apps/v4/examples/aria/data-table-rtl.tsx
Normal file
@@ -0,0 +1,443 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
columnFilteringFeature,
|
||||
columnVisibilityFeature,
|
||||
createColumnHelper,
|
||||
createFilteredRowModel,
|
||||
createPaginatedRowModel,
|
||||
createSortedRowModel,
|
||||
filterFn_includesString,
|
||||
rowPaginationFeature,
|
||||
rowSelectionFeature,
|
||||
rowSortingFeature,
|
||||
sortFn_alphanumeric,
|
||||
sortFn_text,
|
||||
tableFeatures,
|
||||
useTable,
|
||||
type ColumnFiltersState,
|
||||
type ColumnVisibilityState,
|
||||
type SortingState,
|
||||
} from "@tanstack/react-table"
|
||||
import { ArrowUpDown, ChevronDown, MoreHorizontal } from "lucide-react"
|
||||
|
||||
import {
|
||||
useTranslation,
|
||||
type Translations,
|
||||
} from "@/components/language-selector"
|
||||
import { Button } from "@/styles/aria-nova/ui-rtl/button"
|
||||
import { Checkbox } from "@/styles/aria-nova/ui-rtl/checkbox"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/aria-nova/ui-rtl/dropdown-menu"
|
||||
import { Input } from "@/styles/aria-nova/ui-rtl/input"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/styles/aria-nova/ui-rtl/table"
|
||||
import { buttonVariants } from "@/styles/aria-nova/ui/button"
|
||||
|
||||
const translations: Translations = {
|
||||
en: {
|
||||
dir: "ltr",
|
||||
values: {
|
||||
filterEmails: "Filter emails...",
|
||||
columns: "Columns",
|
||||
status: "Status",
|
||||
email: "Email",
|
||||
amount: "Amount",
|
||||
actions: "Actions",
|
||||
copyPaymentId: "Copy payment ID",
|
||||
viewCustomer: "View customer",
|
||||
viewPaymentDetails: "View payment details",
|
||||
selectAll: "Select all",
|
||||
selectRow: "Select row",
|
||||
openMenu: "Open menu",
|
||||
noResults: "No results.",
|
||||
rowsSelected: "of",
|
||||
rowsSelectedSuffix: "row(s) selected.",
|
||||
previous: "Previous",
|
||||
next: "Next",
|
||||
success: "Success",
|
||||
processing: "Processing",
|
||||
failed: "Failed",
|
||||
pending: "Pending",
|
||||
},
|
||||
},
|
||||
ar: {
|
||||
dir: "rtl",
|
||||
values: {
|
||||
filterEmails: "تصفية البريد الإلكتروني...",
|
||||
columns: "الأعمدة",
|
||||
status: "الحالة",
|
||||
email: "البريد الإلكتروني",
|
||||
amount: "المبلغ",
|
||||
actions: "الإجراءات",
|
||||
copyPaymentId: "نسخ معرف الدفع",
|
||||
viewCustomer: "عرض العميل",
|
||||
viewPaymentDetails: "عرض تفاصيل الدفع",
|
||||
selectAll: "تحديد الكل",
|
||||
selectRow: "تحديد الصف",
|
||||
openMenu: "فتح القائمة",
|
||||
noResults: "لا توجد نتائج.",
|
||||
rowsSelected: "من",
|
||||
rowsSelectedSuffix: "صف(وف) محدد.",
|
||||
previous: "السابق",
|
||||
next: "التالي",
|
||||
success: "ناجح",
|
||||
processing: "قيد المعالجة",
|
||||
failed: "فشل",
|
||||
pending: "قيد الانتظار",
|
||||
},
|
||||
},
|
||||
he: {
|
||||
dir: "rtl",
|
||||
values: {
|
||||
filterEmails: "סנן אימיילים...",
|
||||
columns: "עמודות",
|
||||
status: "סטטוס",
|
||||
email: "אימייל",
|
||||
amount: "סכום",
|
||||
actions: "פעולות",
|
||||
copyPaymentId: "העתק מזהה תשלום",
|
||||
viewCustomer: "צפה בלקוח",
|
||||
viewPaymentDetails: "צפה בפרטי תשלום",
|
||||
selectAll: "בחר הכל",
|
||||
selectRow: "בחר שורה",
|
||||
openMenu: "פתח תפריט",
|
||||
noResults: "אין תוצאות.",
|
||||
rowsSelected: "מתוך",
|
||||
rowsSelectedSuffix: "שורות נבחרו.",
|
||||
previous: "הקודם",
|
||||
next: "הבא",
|
||||
success: "הצליח",
|
||||
processing: "מעבד",
|
||||
failed: "נכשל",
|
||||
pending: "ממתין",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// New in v9: declare the features this table uses — anything you don't
|
||||
// register is tree-shaken out of the bundle.
|
||||
const features = tableFeatures({
|
||||
columnFilteringFeature,
|
||||
columnVisibilityFeature,
|
||||
rowPaginationFeature,
|
||||
rowSelectionFeature,
|
||||
rowSortingFeature,
|
||||
filteredRowModel: createFilteredRowModel(),
|
||||
paginatedRowModel: createPaginatedRowModel(),
|
||||
sortedRowModel: createSortedRowModel(),
|
||||
filterFns: { includesString: filterFn_includesString },
|
||||
sortFns: { alphanumeric: sortFn_alphanumeric, text: sortFn_text },
|
||||
})
|
||||
|
||||
const columnHelper = createColumnHelper<typeof features, Payment>()
|
||||
|
||||
type Payment = {
|
||||
id: string
|
||||
amount: number
|
||||
status: "pending" | "processing" | "success" | "failed"
|
||||
email: string
|
||||
}
|
||||
|
||||
const data: Payment[] = [
|
||||
{
|
||||
id: "m5gr84i9",
|
||||
amount: 316,
|
||||
status: "success",
|
||||
email: "ken99@example.com",
|
||||
},
|
||||
{
|
||||
id: "3u1reuv4",
|
||||
amount: 242,
|
||||
status: "success",
|
||||
email: "Abe45@example.com",
|
||||
},
|
||||
{
|
||||
id: "derv1ws0",
|
||||
amount: 837,
|
||||
status: "processing",
|
||||
email: "Monserrat44@example.com",
|
||||
},
|
||||
{
|
||||
id: "5kma53ae",
|
||||
amount: 874,
|
||||
status: "success",
|
||||
email: "Silas22@example.com",
|
||||
},
|
||||
{
|
||||
id: "bhqecj4p",
|
||||
amount: 721,
|
||||
status: "failed",
|
||||
email: "carmella@example.com",
|
||||
},
|
||||
]
|
||||
|
||||
export function DataTableRtl() {
|
||||
const { t, dir, language } = useTranslation(translations, "ar")
|
||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[]
|
||||
)
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
React.useState<ColumnVisibilityState>({})
|
||||
const [rowSelection, setRowSelection] = React.useState({})
|
||||
|
||||
const columns = React.useMemo(
|
||||
() =>
|
||||
columnHelper.columns([
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => <Checkbox slot="selection" />,
|
||||
cell: ({ row }) => <Checkbox slot="selection" />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
}),
|
||||
columnHelper.accessor("status", {
|
||||
header: t.status,
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue("status") as string
|
||||
const statusMap: Record<string, string> = {
|
||||
success: t.success,
|
||||
processing: t.processing,
|
||||
failed: t.failed,
|
||||
pending: t.pending,
|
||||
}
|
||||
return <div className="capitalize">{statusMap[status]}</div>
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("email", {
|
||||
header: () => {
|
||||
return (
|
||||
<div className={buttonVariants({ variant: "ghost" })}>
|
||||
{t.email}
|
||||
<ArrowUpDown />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<div className="lowercase">{row.getValue("email")}</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("amount", {
|
||||
header: () => <div className="text-start">{t.amount}</div>,
|
||||
cell: ({ row }) => {
|
||||
const amount = parseFloat(row.getValue("amount"))
|
||||
const formatted = new Intl.NumberFormat(
|
||||
dir === "rtl" ? "ar-SA" : "en-US",
|
||||
{
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}
|
||||
).format(amount)
|
||||
|
||||
return <div className="text-start font-medium">{formatted}</div>
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => {
|
||||
const payment = row.original
|
||||
|
||||
return (
|
||||
<DropdownMenuTrigger>
|
||||
<Button variant="ghost" size="icon-xs">
|
||||
<span className="sr-only">{t.openMenu}</span>
|
||||
<MoreHorizontal />
|
||||
</Button>
|
||||
<DropdownMenu
|
||||
placement={dir === "rtl" ? "bottom start" : "bottom end"}
|
||||
data-lang={dir === "rtl" ? language : undefined}
|
||||
className="w-40"
|
||||
>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>{t.actions}</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigator.clipboard.writeText(payment.id)}
|
||||
>
|
||||
{t.copyPaymentId}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>{t.viewCustomer}</DropdownMenuItem>
|
||||
<DropdownMenuItem>{t.viewPaymentDetails}</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenu>
|
||||
</DropdownMenuTrigger>
|
||||
)
|
||||
},
|
||||
}),
|
||||
]),
|
||||
[t, dir, language]
|
||||
)
|
||||
|
||||
const table = useTable({
|
||||
features,
|
||||
data,
|
||||
columns,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
columnVisibility,
|
||||
rowSelection,
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex items-center gap-2 py-4">
|
||||
<Input
|
||||
placeholder={t.filterEmails}
|
||||
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
|
||||
onChange={(event) =>
|
||||
table.getColumn("email")?.setFilterValue(event.target.value)
|
||||
}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
<DropdownMenuTrigger>
|
||||
<Button variant="outline" className="ms-auto">
|
||||
{t.columns} <ChevronDown />
|
||||
</Button>
|
||||
<DropdownMenu
|
||||
placement={dir === "rtl" ? "bottom start" : "bottom end"}
|
||||
data-lang={dir === "rtl" ? language : undefined}
|
||||
>
|
||||
<DropdownMenuGroup
|
||||
selectionMode="multiple"
|
||||
selectedKeys={table
|
||||
.getVisibleFlatColumns()
|
||||
.filter((column) => column.getCanHide())
|
||||
.map((column) => column.id)}
|
||||
onSelectionChange={(keys) => {
|
||||
table.setColumnVisibility(
|
||||
Object.fromEntries(
|
||||
table
|
||||
.getAllFlatColumns()
|
||||
.map((c) => [
|
||||
c.id,
|
||||
!c.getCanHide() || keys === "all" || keys.has(c.id),
|
||||
])
|
||||
)
|
||||
)
|
||||
}}
|
||||
>
|
||||
{table
|
||||
.getAllColumns()
|
||||
.filter((column) => column.getCanHide())
|
||||
.map((column) => {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={column.id}
|
||||
id={column.id}
|
||||
className="capitalize"
|
||||
>
|
||||
{column.id}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenu>
|
||||
</DropdownMenuTrigger>
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-md border">
|
||||
<Table
|
||||
aria-label="Tasks"
|
||||
selectionMode="multiple"
|
||||
onSelectionChange={(selection) => {
|
||||
if (selection === "all") {
|
||||
table.toggleAllRowsSelected()
|
||||
} else {
|
||||
table.setRowSelection(
|
||||
Object.fromEntries([...selection].map((key) => [key, true]))
|
||||
)
|
||||
}
|
||||
}}
|
||||
sortDescriptor={
|
||||
sorting.length
|
||||
? {
|
||||
column: sorting[0].id,
|
||||
direction: sorting[0].desc ? "descending" : "ascending",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onSortChange={(sortDescriptor) => {
|
||||
table.setSorting([
|
||||
{
|
||||
id: "" + sortDescriptor.column,
|
||||
desc: sortDescriptor.direction === "descending",
|
||||
},
|
||||
])
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
{table.getFlatHeaders().map((header) => (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
id={header.id}
|
||||
isRowHeader={header.index === 1}
|
||||
allowsSorting={header.column.getCanSort()}
|
||||
>
|
||||
{header.isPlaceholder ? null : (
|
||||
<table.FlexRender header={header} />
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody renderEmptyState={() => t.noResults}>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} id={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
<table.FlexRender cell={cell} />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2 py-4">
|
||||
<div className="flex-1 text-sm text-muted-foreground">
|
||||
{table.getFilteredSelectedRowModel().rows.length} {t.rowsSelected}{" "}
|
||||
{table.getFilteredRowModel().rows.length} {t.rowsSelectedSuffix}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.previousPage()}
|
||||
isDisabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
{t.previous}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.nextPage()}
|
||||
isDisabled={!table.getCanNextPage()}
|
||||
>
|
||||
{t.next}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user