- "content": "\"use client\";\n\nimport type {\n ColumnSort,\n Header,\n SortDirection,\n SortingState,\n Table,\n} from \"@tanstack/react-table\";\nimport {\n ChevronDownIcon,\n ChevronUpIcon,\n EyeOffIcon,\n PinIcon,\n PinOffIcon,\n XIcon,\n} from \"lucide-react\";\nimport * as React from \"react\";\n\nimport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { getColumnVariant } from \"@/lib/data-grid\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataGridColumnHeaderProps<TData, TValue>\n extends React.ComponentProps<typeof DropdownMenuTrigger> {\n header: Header<TData, TValue>;\n table: Table<TData>;\n}\n\nexport function DataGridColumnHeader<TData, TValue>({\n header,\n table,\n className,\n onPointerDown,\n ...props\n}: DataGridColumnHeaderProps<TData, TValue>) {\n const column = header.column;\n const label = column.columnDef.meta?.label\n ? column.columnDef.meta.label\n : typeof column.columnDef.header === \"string\"\n ? column.columnDef.header\n : column.id;\n\n const isAnyColumnResizing =\n table.getState().columnSizingInfo.isResizingColumn;\n\n const cellVariant = column.columnDef.meta?.cell;\n const columnVariant = getColumnVariant(cellVariant?.variant);\n\n const pinnedPosition = column.getIsPinned();\n const isPinnedLeft = pinnedPosition === \"left\";\n const isPinnedRight = pinnedPosition === \"right\";\n\n const onSortingChange = React.useCallback(\n (direction: SortDirection) => {\n table.setSorting((prev: SortingState) => {\n const existingSortIndex = prev.findIndex(\n (sort) => sort.id === column.id,\n );\n const newSort: ColumnSort = {\n id: column.id,\n desc: direction === \"desc\",\n };\n\n if (existingSortIndex >= 0) {\n const updated = [...prev];\n updated[existingSortIndex] = newSort;\n return updated;\n } else {\n return [...prev, newSort];\n }\n });\n },\n [column.id, table],\n );\n\n const onSortRemove = React.useCallback(() => {\n table.setSorting((prev: SortingState) =>\n prev.filter((sort) => sort.id !== column.id),\n );\n }, [column.id, table]);\n\n const onLeftPin = React.useCallback(() => {\n column.pin(\"left\");\n }, [column]);\n\n const onRightPin = React.useCallback(() => {\n column.pin(\"right\");\n }, [column]);\n\n const onUnpin = React.useCallback(() => {\n column.pin(false);\n }, [column]);\n\n const onTriggerPointerDown = React.useCallback(\n (event: React.PointerEvent<HTMLButtonElement>) => {\n onPointerDown?.(event);\n if (event.defaultPrevented) return;\n\n if (event.button !== 0) {\n return;\n }\n table.options.meta?.onColumnClick?.(column.id);\n },\n [table.options.meta, column.id, onPointerDown],\n );\n\n return (\n <>\n <DropdownMenu modal={false}>\n <DropdownMenuTrigger\n className={cn(\n \"flex size-full items-center justify-between gap-2 p-2 text-sm hover:bg-accent/40 data-[state=open]:bg-accent/40 [&_svg]:size-4\",\n isAnyColumnResizing && \"pointer-events-none\",\n className,\n )}\n onPointerDown={onTriggerPointerDown}\n {...props}\n >\n <div className=\"flex min-w-0 flex-1 items-center gap-1.5\">\n {columnVariant && (\n <Tooltip delayDuration={100}>\n <TooltipTrigger asChild>\n <columnVariant.icon className=\"size-3.5 shrink-0 text-muted-foreground\" />\n </TooltipTrigger>\n <TooltipContent side=\"top\">\n <p>{columnVariant.label}</p>\n </TooltipContent>\n </Tooltip>\n )}\n <span className=\"truncate\">{label}</span>\n </div>\n <ChevronDownIcon className=\"shrink-0 text-muted-foreground\" />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" sideOffset={0} className=\"w-60\">\n {column.getCanSort() && (\n <>\n <DropdownMenuCheckboxItem\n className=\"relative ltr:pr-8 ltr:pl-2 rtl:pr-2 rtl:pl-8 [&>span:first-child]:ltr:right-2 [&>span:first-child]:ltr:left-auto [&>span:first-child]:rtl:right-auto [&>span:first-child]:rtl:left-2 [&_svg]:text-muted-foreground\"\n checked={column.getIsSorted() === \"asc\"}\n onClick={() => onSortingChange(\"asc\")}\n >\n <ChevronUpIcon />\n Sort asc\n </DropdownMenuCheckboxItem>\n <DropdownMenuCheckboxItem\n className=\"relative ltr:pr-8 ltr:pl-2 rtl:pr-2 rtl:pl-8 [&>span:first-child]:ltr:right-2 [&>span:first-child]:ltr:left-auto [&>span:first-child]:rtl:right-auto [&>span:first-child]:rtl:left-2 [&_svg]:text-muted-foreground\"\n checked={column.getIsSorted() === \"desc\"}\n onClick={() => onSortingChange(\"desc\")}\n >\n <ChevronDownIcon />\n Sort desc\n </DropdownMenuCheckboxItem>\n {column.getIsSorted() && (\n <DropdownMenuItem onClick={onSortRemove}>\n <XIcon />\n Remove sort\n </DropdownMenuItem>\n )}\n </>\n )}\n {column.getCanPin() && (\n <>\n {column.getCanSort() && <DropdownMenuSeparator />}\n\n {isPinnedLeft ? (\n <DropdownMenuItem\n className=\"[&_svg]:text-muted-foreground\"\n onClick={onUnpin}\n >\n <PinOffIcon />\n Unpin from left\n </DropdownMenuItem>\n ) : (\n <DropdownMenuItem\n className=\"[&_svg]:text-muted-foreground\"\n onClick={onLeftPin}\n >\n <PinIcon />\n Pin to left\n </DropdownMenuItem>\n )}\n {isPinnedRight ? (\n <DropdownMenuItem\n className=\"[&_svg]:text-muted-foreground\"\n onClick={onUnpin}\n >\n <PinOffIcon />\n Unpin from right\n </DropdownMenuItem>\n ) : (\n <DropdownMenuItem\n className=\"[&_svg]:text-muted-foreground\"\n onClick={onRightPin}\n >\n <PinIcon />\n Pin to right\n </DropdownMenuItem>\n )}\n </>\n )}\n {column.getCanHide() && (\n <>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n className=\"[&_svg]:text-muted-foreground\"\n onClick={() => column.toggleVisibility(false)}\n >\n <EyeOffIcon />\n Hide column\n </DropdownMenuItem>\n </>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n {header.column.getCanResize() && (\n <DataGridColumnResizer header={header} table={table} label={label} />\n )}\n </>\n );\n}\n\nconst DataGridColumnResizer = React.memo(\n DataGridColumnResizerImpl,\n (prev, next) => {\n const prevColumn = prev.header.column;\n const nextColumn = next.header.column;\n\n if (\n prevColumn.getIsResizing() !== nextColumn.getIsResizing() ||\n prevColumn.getSize() !== nextColumn.getSize()\n ) {\n return false;\n }\n\n if (prev.label !== next.label) return false;\n\n return true;\n },\n) as typeof DataGridColumnResizerImpl;\n\ninterface DataGridColumnResizerProps<TData, TValue>\n extends DataGridColumnHeaderProps<TData, TValue> {\n label: string;\n}\n\nfunction DataGridColumnResizerImpl<TData, TValue>({\n header,\n table,\n label,\n}: DataGridColumnResizerProps<TData, TValue>) {\n const defaultColumnDef = table._getDefaultColumnDef();\n\n const onDoubleClick = React.useCallback(() => {\n header.column.resetSize();\n }, [header.column]);\n\n return (\n <div\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label={`Resize ${label} column`}\n aria-valuenow={header.column.getSize()}\n aria-valuemin={defaultColumnDef.minSize}\n aria-valuemax={defaultColumnDef.maxSize}\n tabIndex={0}\n className={cn(\n \"absolute -end-px top-0 z-50 h-full w-0.5 cursor-ew-resize touch-none select-none bg-border transition-opacity after:absolute after:inset-y-0 after:start-1/2 after:h-full after:w-[18px] after:-translate-x-1/2 after:content-[''] hover:bg-primary focus:bg-primary focus:outline-none\",\n header.column.getIsResizing()\n ? \"bg-primary\"\n : \"opacity-0 hover:opacity-100\",\n )}\n onDoubleClick={onDoubleClick}\n onMouseDown={header.getResizeHandler()}\n onTouchStart={header.getResizeHandler()}\n />\n );\n}\n",
0 commit comments