Skip to content
Merged
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
2 changes: 1 addition & 1 deletion public/r/data-grid-select-column.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"files": [
{
"path": "src/components/data-grid/data-grid-select-column.tsx",
"content": "\"use client\";\n\nimport type {\n CellContext,\n ColumnDef,\n HeaderContext,\n} from \"@tanstack/react-table\";\nimport * as React from \"react\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { cn } from \"@/lib/utils\";\n\ntype HitboxSize = \"default\" | \"sm\" | \"lg\";\n\ninterface DataGridSelectHitboxProps {\n htmlFor: string;\n children: React.ReactNode;\n size?: HitboxSize;\n debug?: boolean;\n}\n\nfunction DataGridSelectHitbox({\n htmlFor,\n children,\n size,\n debug,\n}: DataGridSelectHitboxProps) {\n return (\n <div\n className={cn(\n \"group relative -my-1.5 h-[calc(100%+0.75rem)] py-1.5\",\n size === \"default\" && \"-ms-3 -me-2 ps-3 pe-2\",\n size === \"sm\" && \"-ms-3 -me-1.5 ps-3 pe-1.5\",\n size === \"lg\" && \"-mx-3 px-3\",\n )}\n >\n {children}\n <label\n htmlFor={htmlFor}\n className={cn(\n \"absolute inset-0 cursor-pointer\",\n debug && \"border border-red-500 border-dashed bg-red-500/20\",\n )}\n />\n </div>\n );\n}\n\ninterface DataGridSelectCheckboxProps\n extends Omit<React.ComponentProps<typeof Checkbox>, \"id\"> {\n rowNumber?: number;\n hitboxSize?: HitboxSize;\n debug?: boolean;\n}\n\nfunction DataGridSelectCheckbox({\n rowNumber,\n hitboxSize,\n debug,\n checked,\n className,\n ...props\n}: DataGridSelectCheckboxProps) {\n const id = React.useId();\n\n if (rowNumber !== undefined) {\n return (\n <DataGridSelectHitbox htmlFor={id} size={hitboxSize} debug={debug}>\n <div\n aria-hidden=\"true\"\n className={cn(\n \"pointer-events-none absolute start-3 top-1.5 flex size-4 items-center justify-center text-muted-foreground text-xs tabular-nums transition-opacity group-hover:opacity-0\",\n checked && \"opacity-0\",\n )}\n >\n {rowNumber}\n </div>\n <Checkbox\n id={id}\n className={cn(\n \"relative transition-[shadow,border,opacity] hover:border-primary/40\",\n \"opacity-0 group-hover:opacity-100 data-[state=checked]:opacity-100\",\n className,\n )}\n checked={checked}\n {...props}\n />\n </DataGridSelectHitbox>\n );\n }\n\n return (\n <DataGridSelectHitbox htmlFor={id} size={hitboxSize} debug={debug}>\n <Checkbox\n id={id}\n className={cn(\n \"relative transition-[shadow,border] hover:border-primary/40\",\n className,\n )}\n checked={checked}\n {...props}\n />\n </DataGridSelectHitbox>\n );\n}\n\ninterface DataGridSelectHeaderProps<TData>\n extends Pick<HeaderContext<TData, unknown>, \"table\"> {\n hitboxSize?: HitboxSize;\n debug?: boolean;\n}\n\nfunction DataGridSelectHeader<TData>({\n table,\n hitboxSize,\n debug,\n}: DataGridSelectHeaderProps<TData>) {\n const onCheckedChange = React.useCallback(\n (value: boolean) => table.toggleAllPageRowsSelected(value),\n [table],\n );\n\n return (\n <DataGridSelectCheckbox\n aria-label=\"Select all\"\n checked={\n table.getIsAllPageRowsSelected() ||\n (table.getIsSomePageRowsSelected() && \"indeterminate\")\n }\n onCheckedChange={onCheckedChange}\n hitboxSize={hitboxSize}\n debug={debug}\n />\n );\n}\n\ninterface DataGridSelectCellProps<TData>\n extends Pick<CellContext<TData, unknown>, \"row\" | \"table\"> {\n hitboxSize?: HitboxSize;\n enableRowMarkers?: boolean;\n debug?: boolean;\n}\n\nfunction DataGridSelectCell<TData>({\n row,\n table,\n hitboxSize,\n enableRowMarkers,\n debug,\n}: DataGridSelectCellProps<TData>) {\n const meta = table.options.meta;\n const rowNumber = enableRowMarkers\n ? (meta?.getVisualRowIndex?.(row.id) ?? row.index + 1)\n : undefined;\n\n const onCheckedChange = React.useCallback(\n (value: boolean) => {\n if (meta?.onRowSelect) {\n meta.onRowSelect(row.index, value, false);\n } else {\n row.toggleSelected(value);\n }\n },\n [meta, row],\n );\n\n const onClick = React.useCallback(\n (event: React.MouseEvent<HTMLButtonElement>) => {\n if (event.shiftKey) {\n event.preventDefault();\n meta?.onRowSelect?.(row.index, !row.getIsSelected(), true);\n }\n },\n [meta, row],\n );\n\n return (\n <DataGridSelectCheckbox\n aria-label={rowNumber ? `Select row ${rowNumber}` : \"Select row\"}\n checked={row.getIsSelected()}\n onCheckedChange={onCheckedChange}\n onClick={onClick}\n rowNumber={rowNumber}\n hitboxSize={hitboxSize}\n debug={debug}\n />\n );\n}\n\ninterface GetDataGridSelectColumnOptions<TData>\n extends Omit<Partial<ColumnDef<TData>>, \"id\" | \"header\" | \"cell\"> {\n enableRowMarkers?: boolean;\n hitboxSize?: HitboxSize;\n debug?: boolean;\n}\n\nexport function getDataGridSelectColumn<TData>({\n size = 40,\n hitboxSize = \"default\",\n enableHiding = false,\n enableResizing = false,\n enableSorting = false,\n enableRowMarkers = false,\n debug = false,\n ...props\n}: GetDataGridSelectColumnOptions<TData> = {}): ColumnDef<TData> {\n return {\n id: \"select\",\n header: ({ table }) => (\n <DataGridSelectHeader\n table={table}\n hitboxSize={hitboxSize}\n debug={debug}\n />\n ),\n cell: ({ row, table }) => (\n <DataGridSelectCell\n row={row}\n table={table}\n enableRowMarkers={enableRowMarkers}\n hitboxSize={hitboxSize}\n debug={debug}\n />\n ),\n size,\n enableHiding,\n enableResizing,\n enableSorting,\n ...props,\n };\n}\n",
"content": "\"use client\";\n\nimport type {\n CellContext,\n ColumnDef,\n HeaderContext,\n} from \"@tanstack/react-table\";\nimport * as React from \"react\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { cn } from \"@/lib/utils\";\n\ntype HitboxSize = \"default\" | \"sm\" | \"lg\";\n\ninterface DataGridSelectHitboxProps {\n htmlFor: string;\n children: React.ReactNode;\n size?: HitboxSize;\n debug?: boolean;\n}\n\nfunction DataGridSelectHitbox({\n htmlFor,\n children,\n size,\n debug,\n}: DataGridSelectHitboxProps) {\n return (\n <div\n className={cn(\n \"group relative -my-1.5 h-[calc(100%+0.75rem)] py-1.5\",\n size === \"default\" && \"-ms-3 -me-2 ps-3 pe-2\",\n size === \"sm\" && \"-ms-3 -me-1.5 ps-3 pe-1.5\",\n size === \"lg\" && \"-mx-3 px-3\",\n )}\n >\n {children}\n <label\n htmlFor={htmlFor}\n className={cn(\n \"absolute inset-0 cursor-pointer\",\n debug && \"border border-red-500 border-dashed bg-red-500/20\",\n )}\n />\n </div>\n );\n}\n\ninterface DataGridSelectCheckboxProps\n extends Omit<React.ComponentProps<typeof Checkbox>, \"id\"> {\n rowNumber?: number;\n hitboxSize?: HitboxSize;\n debug?: boolean;\n}\n\nfunction DataGridSelectCheckbox({\n rowNumber,\n hitboxSize,\n debug,\n checked,\n className,\n ...props\n}: DataGridSelectCheckboxProps) {\n const id = React.useId();\n\n if (rowNumber !== undefined) {\n return (\n <DataGridSelectHitbox htmlFor={id} size={hitboxSize} debug={debug}>\n <div\n aria-hidden=\"true\"\n className={cn(\n \"pointer-events-none absolute start-3 top-1.5 flex size-4 items-center justify-center text-muted-foreground text-xs tabular-nums transition-opacity group-hover:opacity-0\",\n checked && \"opacity-0\",\n )}\n >\n {rowNumber}\n </div>\n <Checkbox\n id={id}\n className={cn(\n \"relative transition-[shadow,border,opacity] hover:border-primary/40\",\n \"opacity-0 group-hover:opacity-100 data-[state=checked]:opacity-100\",\n className,\n )}\n checked={checked}\n {...props}\n />\n </DataGridSelectHitbox>\n );\n }\n\n return (\n <DataGridSelectHitbox htmlFor={id} size={hitboxSize} debug={debug}>\n <Checkbox\n id={id}\n className={cn(\n \"relative transition-[shadow,border] hover:border-primary/40\",\n className,\n )}\n checked={checked}\n {...props}\n />\n </DataGridSelectHitbox>\n );\n}\n\ninterface DataGridSelectHeaderProps<TData>\n extends Pick<HeaderContext<TData, unknown>, \"table\"> {\n hitboxSize?: HitboxSize;\n readOnly?: boolean;\n debug?: boolean;\n}\n\nfunction DataGridSelectHeader<TData>({\n table,\n hitboxSize,\n readOnly,\n debug,\n}: DataGridSelectHeaderProps<TData>) {\n const onCheckedChange = React.useCallback(\n (value: boolean) => table.toggleAllPageRowsSelected(value),\n [table],\n );\n\n if (readOnly) {\n return (\n <div className=\"mt-1 flex items-center ps-1 text-muted-foreground text-sm\">\n #\n </div>\n );\n }\n\n return (\n <DataGridSelectCheckbox\n aria-label=\"Select all\"\n checked={\n table.getIsAllPageRowsSelected() ||\n (table.getIsSomePageRowsSelected() && \"indeterminate\")\n }\n onCheckedChange={onCheckedChange}\n hitboxSize={hitboxSize}\n debug={debug}\n />\n );\n}\n\ninterface DataGridSelectCellProps<TData>\n extends Pick<CellContext<TData, unknown>, \"row\" | \"table\"> {\n hitboxSize?: HitboxSize;\n enableRowMarkers?: boolean;\n readOnly?: boolean;\n debug?: boolean;\n}\n\nfunction DataGridSelectCell<TData>({\n row,\n table,\n hitboxSize,\n enableRowMarkers,\n readOnly,\n debug,\n}: DataGridSelectCellProps<TData>) {\n const meta = table.options.meta;\n const rowNumber = enableRowMarkers\n ? (meta?.getVisualRowIndex?.(row.id) ?? row.index + 1)\n : undefined;\n\n const onCheckedChange = React.useCallback(\n (value: boolean) => {\n if (meta?.onRowSelect) {\n meta.onRowSelect(row.index, value, false);\n } else {\n row.toggleSelected(value);\n }\n },\n [meta, row],\n );\n\n const onClick = React.useCallback(\n (event: React.MouseEvent<HTMLButtonElement>) => {\n if (event.shiftKey) {\n event.preventDefault();\n meta?.onRowSelect?.(row.index, !row.getIsSelected(), true);\n }\n },\n [meta, row],\n );\n\n if (readOnly) {\n return (\n <div className=\"flex items-center ps-1 text-muted-foreground text-xs tabular-nums\">\n {rowNumber ?? row.index + 1}\n </div>\n );\n }\n\n return (\n <DataGridSelectCheckbox\n aria-label={rowNumber ? `Select row ${rowNumber}` : \"Select row\"}\n checked={row.getIsSelected()}\n onCheckedChange={onCheckedChange}\n onClick={onClick}\n rowNumber={rowNumber}\n hitboxSize={hitboxSize}\n debug={debug}\n />\n );\n}\n\ninterface GetDataGridSelectColumnOptions<TData>\n extends Omit<Partial<ColumnDef<TData>>, \"id\" | \"header\" | \"cell\"> {\n enableRowMarkers?: boolean;\n readOnly?: boolean;\n hitboxSize?: HitboxSize;\n debug?: boolean;\n}\n\nexport function getDataGridSelectColumn<TData>({\n size = 40,\n hitboxSize = \"default\",\n enableHiding = false,\n enableResizing = false,\n enableSorting = false,\n enableRowMarkers = false,\n readOnly = false,\n debug = false,\n ...props\n}: GetDataGridSelectColumnOptions<TData> = {}): ColumnDef<TData> {\n return {\n id: \"select\",\n header: ({ table }) => (\n <DataGridSelectHeader\n table={table}\n hitboxSize={hitboxSize}\n readOnly={readOnly}\n debug={debug}\n />\n ),\n cell: ({ row, table }) => (\n <DataGridSelectCell\n row={row}\n table={table}\n enableRowMarkers={enableRowMarkers}\n readOnly={readOnly}\n hitboxSize={hitboxSize}\n debug={debug}\n />\n ),\n size,\n enableHiding,\n enableResizing,\n enableSorting,\n ...props,\n };\n}\n",
"type": "registry:component",
"target": "src/components/data-grid/data-grid-select-column.tsx"
}
Expand Down
119 changes: 119 additions & 0 deletions src/app/data-grid-live/components/data-grid-action-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"use client";

import type { Table, TableMeta } from "@tanstack/react-table";
import { CheckCircle2, Palette, Trash2, X } from "lucide-react";
import * as React from "react";

import {
ActionBar,
ActionBarClose,
ActionBarGroup,
ActionBarItem,
ActionBarSelection,
ActionBarSeparator,
} from "@/components/ui/action-bar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import type { CellSelectOption } from "@/types/data-grid";

interface DataGridActionBarProps<TData> {
table: Table<TData>;
tableMeta: TableMeta<TData>;
selectedCellCount: number;
statusOptions?: CellSelectOption[];
styleOptions?: CellSelectOption[];
onStatusUpdate?: (value: string) => void;
onStyleUpdate?: (value: string) => void;
onDelete?: () => void;
}

export function DataGridActionBar<TData>({
table,
tableMeta,
selectedCellCount,
statusOptions,
styleOptions,
onStatusUpdate,
onStyleUpdate,
onDelete,
}: DataGridActionBarProps<TData>) {
const onOpenChange = React.useCallback(
(open: boolean) => {
if (!open) {
table.toggleAllRowsSelected(false);
tableMeta.onSelectionClear?.();
}
},
[table, tableMeta],
);

return (
<ActionBar
data-grid-popover
open={selectedCellCount > 0}
onOpenChange={onOpenChange}
>
<ActionBarSelection>
<span className="font-medium">{selectedCellCount}</span>
<span>{selectedCellCount === 1 ? "cell" : "cells"} selected</span>
<ActionBarSeparator />
<ActionBarClose>
<X />
</ActionBarClose>
</ActionBarSelection>
<ActionBarSeparator />
<ActionBarGroup>
{statusOptions && statusOptions.length > 0 && onStatusUpdate && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ActionBarItem variant="secondary" size="sm">
<CheckCircle2 />
Status
</ActionBarItem>
</DropdownMenuTrigger>
<DropdownMenuContent data-grid-popover>
{statusOptions.map((option) => (
<DropdownMenuItem
key={option.value}
onClick={() => onStatusUpdate(option.value)}
>
{option.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)}
{styleOptions && styleOptions.length > 0 && onStyleUpdate && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ActionBarItem variant="secondary" size="sm">
<Palette />
Style
</ActionBarItem>
</DropdownMenuTrigger>
<DropdownMenuContent data-grid-popover>
{styleOptions.map((option) => (
<DropdownMenuItem
key={option.value}
onClick={() => onStyleUpdate(option.value)}
>
{option.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)}
{onDelete && (
<ActionBarItem variant="destructive" size="sm" onClick={onDelete}>
<Trash2 />
Delete
</ActionBarItem>
)}
</ActionBarGroup>
</ActionBar>
);
}
Loading
Loading