feat(base): row-number cell renders checkbox + drag handle on hover

This commit is contained in:
Philipinho
2026-04-18 16:40:21 +01:00
parent fda163311a
commit 3fca962c9f
5 changed files with 139 additions and 11 deletions
@@ -18,6 +18,7 @@ import { CellFile } from "@/features/base/components/cells/cell-file";
import { CellCreatedAt } from "@/features/base/components/cells/cell-created-at";
import { CellLastEditedAt } from "@/features/base/components/cells/cell-last-edited-at";
import { CellLastEditedBy } from "@/features/base/components/cells/cell-last-edited-by";
import { RowNumberCell } from "./row-number-cell";
import classes from "@/features/base/styles/grid.module.css";
type CellComponentProps = {
@@ -59,6 +60,7 @@ type GridCellProps = {
rowIndex: number;
onCellUpdate: (rowId: string, propertyId: string, value: unknown) => void;
rowDragProps?: RowDragProps;
orderedRowIds?: string[];
};
export const GridCell = memo(function GridCell({
@@ -66,6 +68,7 @@ export const GridCell = memo(function GridCell({
rowIndex,
onCellUpdate,
rowDragProps,
orderedRowIds,
}: GridCellProps) {
const property = cell.column.columnDef.meta?.property;
const isRowNumber = cell.column.id === "__row_number";
@@ -107,16 +110,14 @@ export const GridCell = memo(function GridCell({
if (isRowNumber) {
return (
<div
className={`${classes.cell} ${classes.rowNumberCell} ${isPinned ? classes.cellPinned : ""} ${rowDragProps ? classes.rowNumberDraggable : ""}`}
style={{
...(isPinned ? { left: pinOffset } : {}),
}}
draggable={rowDragProps?.draggable}
onDragStart={rowDragProps?.onDragStart}
>
{rowIndex + 1}
</div>
<RowNumberCell
rowId={rowId}
rowIndex={rowIndex}
orderedRowIds={orderedRowIds ?? []}
isPinned={Boolean(isPinned)}
pinOffset={pinOffset}
rowDragProps={rowDragProps}
/>
);
}
@@ -227,6 +227,7 @@ export function GridContainer({
row={row}
rowIndex={virtualRow.index}
onCellUpdate={onCellUpdate}
orderedRowIds={rowIds}
dragHandlers={
onRowReorder
? {
@@ -1,6 +1,7 @@
import { memo, useCallback } from "react";
import { Row } from "@tanstack/react-table";
import { IBaseRow } from "@/features/base/types/base.types";
import { useRowSelection } from "@/features/base/hooks/use-row-selection";
import { GridCell } from "./grid-cell";
import classes from "@/features/base/styles/grid.module.css";
@@ -19,6 +20,7 @@ type GridRowProps = {
rowIndex: number;
onCellUpdate: (rowId: string, propertyId: string, value: unknown) => void;
dragHandlers?: RowDragHandlers;
orderedRowIds: string[];
};
export const GridRow = memo(function GridRow({
@@ -26,7 +28,9 @@ export const GridRow = memo(function GridRow({
rowIndex,
onCellUpdate,
dragHandlers,
orderedRowIds,
}: GridRowProps) {
const isSelected = useRowSelection().isSelected(row.id);
const handleDragStart = useCallback(
(e: React.DragEvent) => {
e.dataTransfer.effectAllowed = "move";
@@ -51,7 +55,7 @@ export const GridRow = memo(function GridRow({
return (
<div
className={`${classes.row} ${dragHandlers?.isDragging ? classes.rowDragging : ""} ${dropIndicatorClass}`}
className={`${classes.row} ${dragHandlers?.isDragging ? classes.rowDragging : ""} ${dropIndicatorClass} ${isSelected ? classes.rowSelected : ""}`}
role="row"
onDragOver={handleDragOver}
onDrop={(e) => {
@@ -68,6 +72,7 @@ export const GridRow = memo(function GridRow({
cell={cell}
rowIndex={rowIndex}
onCellUpdate={onCellUpdate}
orderedRowIds={orderedRowIds}
rowDragProps={
isRowNumber && dragHandlers
? {
@@ -0,0 +1,70 @@
import { memo, useCallback } from "react";
import { Checkbox } from "@mantine/core";
import { IconGripVertical } from "@tabler/icons-react";
import { useRowSelection } from "@/features/base/hooks/use-row-selection";
import classes from "@/features/base/styles/grid.module.css";
type RowDragProps = {
draggable: boolean;
onDragStart: (e: React.DragEvent) => void;
};
type RowNumberCellProps = {
rowId: string;
rowIndex: number;
orderedRowIds: string[];
isPinned: boolean;
pinOffset?: number;
rowDragProps?: RowDragProps;
};
export const RowNumberCell = memo(function RowNumberCell({
rowId,
rowIndex,
orderedRowIds,
isPinned,
pinOffset,
rowDragProps,
}: RowNumberCellProps) {
const { isSelected, toggle } = useRowSelection();
const selected = isSelected(rowId);
const handleCheckboxChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const nativeEvent = e.nativeEvent as MouseEvent;
toggle(rowId, {
shiftKey: nativeEvent.shiftKey === true,
rowIndex,
orderedRowIds,
});
},
[rowId, rowIndex, orderedRowIds, toggle],
);
return (
<div
className={`${classes.cell} ${classes.rowNumberCell} ${isPinned ? classes.cellPinned : ""}`}
style={isPinned ? { left: pinOffset } : undefined}
>
<div className={classes.rowNumberCellInner}>
<span
className={classes.rowNumberDragHandle}
draggable={rowDragProps?.draggable}
onDragStart={rowDragProps?.onDragStart}
aria-label="Drag row"
>
<IconGripVertical size={12} />
</span>
<span className={classes.rowNumberCheckbox}>
<Checkbox
size="xs"
checked={selected}
onChange={handleCheckboxChange}
aria-label="Select row"
/>
</span>
<span className={classes.rowNumberIndex}>{rowIndex + 1}</span>
</div>
</div>
);
});
@@ -296,3 +296,54 @@
.primaryCell {
font-weight: 500;
}
.rowNumberCellInner {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
width: 100%;
height: 100%;
padding: 0 6px;
}
.rowNumberIndex {
display: inline;
}
.rowNumberCheckbox,
.rowNumberDragHandle {
display: none;
align-items: center;
justify-content: center;
}
.rowNumberDragHandle {
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
cursor: grab;
}
.rowNumberDragHandle:active {
cursor: grabbing;
}
/* On hover, hide the index and show drag handle + checkbox */
.row:hover .rowNumberIndex {
display: none;
}
.row:hover .rowNumberCheckbox,
.row:hover .rowNumberDragHandle {
display: inline-flex;
}
/* When selected, checkbox always visible; index + drag handle hidden */
.rowSelected .rowNumberIndex,
.rowSelected .rowNumberDragHandle {
display: none;
}
.rowSelected .rowNumberCheckbox {
display: inline-flex;
}
.rowSelected .cell {
background: light-dark(var(--mantine-color-blue-0), var(--mantine-color-dark-6));
}