Row Pinning
Row pinning keeps rows visible at the top or bottom edge of the table.
Pin | Username | Account Status | Role | Avg Session Duration | Company Name | Last Visited At | Visit Count |
|---|
{
"rowPinning": {
"bottom": [],
"top": []
},
"rowSelection": {}
}
import {useMemo, useState} from "react"
import {ArrowDown, ArrowUp, ChevronDown, ChevronRight, X} from "lucide-react"
import {
type CellContext,
type Column,
type ColumnDef,
type ExpandedState,
getCoreRowModel,
getExpandedRowModel,
getFilteredRowModel,
getPaginationRowModel,
type Row,
type RowPinningState,
type TableInstance,
} from "@qualcomm-ui/core/table"
import {Button} from "@qualcomm-ui/react/button"
import {Checkbox} from "@qualcomm-ui/react/checkbox"
import {NumberInput} from "@qualcomm-ui/react/number-input"
import {Pagination} from "@qualcomm-ui/react/pagination"
import {ProgressRing} from "@qualcomm-ui/react/progress-ring"
import {
flexRender,
Table,
useReactTable,
useTablePagination,
} from "@qualcomm-ui/react/table"
import {TextInput} from "@qualcomm-ui/react/text-input"
import {CodeHighlight} from "@qualcomm-ui/react-mdx/code-highlight"
import {type User, useUserData} from "./use-data"
export function RowPinningDemo() {
// table states
const [rowPinning, setRowPinning] = useState<RowPinningState>({
bottom: [],
top: [],
})
const [expanded, setExpanded] = useState<ExpandedState>({})
// demo states
const [keepPinnedRows, setKeepPinnedRows] = useState(false)
const [includeLeafRows, setIncludeLeafRows] = useState(true)
const [includeParentRows, setIncludeParentRows] = useState(true)
const columns: ColumnDef<User>[] = useMemo<ColumnDef<User>[]>(
() => [
{
cell: ({row}) =>
row.getIsPinned() ? (
<Table.CellAction
icon={X}
onClick={() => row.pin(false, includeLeafRows, includeParentRows)}
/>
) : (
<div className="flex gap-1">
<Table.CellAction
icon={ArrowUp}
onClick={() =>
row.pin("top", includeLeafRows, includeParentRows)
}
/>
<Table.CellAction
icon={ArrowDown}
onClick={() =>
row.pin("bottom", includeLeafRows, includeParentRows)
}
/>
</div>
),
header: "Pin",
id: "pin",
},
{
accessorKey: "username",
cell: ({getValue, row}: CellContext<User, any>) => {
return (
<div
className="inline-flex items-center gap-2"
style={{
// Since rows are flattened by default,
// we can use the row.depth property
// and paddingLeft to visually indicate the depth
// of the row
paddingLeft: `${row.depth * 2}rem`,
}}
>
<>
{row.getCanExpand() ? (
<Table.RowExpandButton row={row} />
) : null}
<span>{getValue()}</span>
</>
</div>
)
},
header: ({table}) => {
return (
<div className="flex items-center gap-2">
<Table.ColumnHeaderAction
icon={table.getIsAllRowsExpanded() ? ChevronDown : ChevronRight}
onClick={table.getToggleAllRowsExpandedHandler()}
/>
<span>Username</span>
</div>
)
},
},
{
accessorKey: "accountStatus",
header: "Account Status",
id: "accountStatus",
},
{
accessorKey: "role",
header: "Role",
id: "role",
},
{
accessorKey: "averageSessionDuration",
header: "Avg Session Duration",
id: "averageSessionDuration",
},
{
accessorKey: "companyName",
header: "Company Name",
id: "companyName",
minSize: 220,
},
{
accessorKey: "lastVisitedAt",
header: "Last Visited At",
id: "lastVisitedAt",
minSize: 205,
},
{
accessorKey: "visitCount",
header: "Visit Count",
id: "visitCount",
},
],
[includeLeafRows, includeParentRows],
)
const {data = [], isFetching, refetch} = useUserData(1000, 2, 2)
const refreshData = () => refetch()
const table = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
getExpandedRowModel: getExpandedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSubRows: (row) => row.subRows,
initialState: {pagination: {pageIndex: 0, pageSize: 10}},
keepPinnedRows,
onExpandedChange: setExpanded,
onRowPinningChange: setRowPinning,
state: {
expanded,
rowPinning,
},
})
const paginationProps = useTablePagination(table)
return (
<div className="flex w-full flex-col gap-4 p-2">
<div className="align-center vertical flex flex-col gap-2">
<Checkbox
checked={includeParentRows}
label="Include Parent Rows When Pinning Child"
onCheckedChange={(checked) => setIncludeParentRows(checked)}
size="sm"
/>
<Checkbox
checked={includeLeafRows}
label="Include Leaf Rows When Pinning Parent"
onCheckedChange={(checked) => setIncludeLeafRows(checked)}
size="sm"
/>
<Checkbox
checked={keepPinnedRows}
label="Persist Pinned Rows across Pagination and Filtering"
onCheckedChange={(checked) => setKeepPinnedRows(checked)}
size="sm"
/>
</div>
<Table.Root>
<Table.ActionBar>
<Button onClick={() => void refreshData()} variant="outline">
Refresh Data
</Button>
{isFetching ? <ProgressRing size="xs" /> : null}
</Table.ActionBar>
<Table.ScrollContainer>
<Table.Table>
<Table.Header>
{table.getHeaderGroups().map((headerGroup) => (
<Table.Row key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<Table.HeaderCell
key={header.id}
colSpan={header.colSpan}
>
{header.isPlaceholder ? null : (
<div className="inline-flex flex-col gap-1">
<div className="inline-flex min-h-[28px] items-center justify-center">
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</div>
{header.column.getCanFilter() ? (
<Filter column={header.column} table={table} />
) : null}
</div>
)}
</Table.HeaderCell>
)
})}
</Table.Row>
))}
</Table.Header>
<Table.Body>
{table.getTopRows().map((row) => (
<PinnedRow key={row.id} row={row} table={table} />
))}
{table.getCenterRows().map((row) => {
return (
<Table.Row key={row.id} isSelected={row.getIsSelected()}>
{row.getVisibleCells().map((cell) => {
return (
<Table.Cell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</Table.Cell>
)
})}
</Table.Row>
)
})}
{table.getBottomRows().map((row) => (
<PinnedRow key={row.id} row={row} table={table} />
))}
</Table.Body>
</Table.Table>
</Table.ScrollContainer>
<Table.Pagination {...paginationProps}>
<Pagination.PageMetadata>
{({count, pageEnd, pageStart}) => (
<>
{pageStart}-{pageEnd} of {count} results
</>
)}
</Pagination.PageMetadata>
<Pagination.PageButtons />
</Table.Pagination>
</Table.Root>
<CodeHighlight
className="w-fit"
code={JSON.stringify(
{
rowPinning: table.getState().rowPinning,
rowSelection: table.getState().rowSelection,
},
null,
2,
)}
disableCopy
language="json"
/>
</div>
)
}
function PinnedRow({row, table}: {row: Row<User>; table: TableInstance<User>}) {
return (
<Table.Row
className="sticky"
isSelected
style={{
bottom:
row.getIsPinned() === "bottom"
? `${
(table.getBottomRows().length - 1 - row.getPinnedIndex()) * 26
}px`
: undefined,
top:
row.getIsPinned() === "top"
? `${row.getPinnedIndex() * 26 + 48}px`
: undefined,
}}
>
{row.getVisibleCells().map((cell) => {
return (
<Table.Cell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</Table.Cell>
)
})}
</Table.Row>
)
}
interface FilterProps {
column: Column<User>
table: TableInstance<User>
}
function Filter({column, table}: FilterProps) {
const firstValue = table
.getPreFilteredRowModel()
.flatRows[0]?.getValue(column.id)
return typeof firstValue === "number" ? (
<MinMaxNumberFilter column={column} table={table} />
) : (
<TextInput
className="w-32"
onValueChange={(value) => column.setFilterValue(value)}
placeholder="Search..."
size="sm"
value={(column.getFilterValue() as string) ?? ""}
/>
)
}
function MinMaxNumberFilter({column}: FilterProps) {
const columnFilterValue = column.getFilterValue() as [number, number]
const [min, max] = columnFilterValue ?? [0, 0]
return (
<div className="flex w-32 gap-2">
<NumberInput
controlProps={{hidden: true}}
min={0}
onValueChange={({valueAsNumber}) =>
column.setFilterValue((old: [number, number]) => [
valueAsNumber,
old?.[1],
])
}
placeholder="Min"
size="sm"
value={min ? `${min}` : ""}
/>
<NumberInput
controlProps={{hidden: true}}
max={130}
onValueChange={({valueAsNumber}) =>
column.setFilterValue((old: [number, number]) => [
old?.[0],
valueAsNumber,
])
}
placeholder="Max"
size="sm"
value={max ? `${max}` : ""}
/>
</div>
)
}