Skip to main content

UX Standards — 07B: UDT Cell Selection and Copy

Governs: Cell-level selection, range selection, and the right-click context menu copy operations. Parent rules: See 07-UNIVERSAL-DATA-TABLE.md and 00-OVERVIEW-AND-CSS-RULES.md first.


Overview

The UDT supports fine-grained data selection at four levels:

  1. Row selection — checkbox column (for toolbar actions like Edit, Delete)
  2. Cell selection — click a single cell to focus it
  3. Range selection — click-drag across multiple cells in multiple rows/columns
  4. Context menu copy — right-click any selected area to copy specific data scopes

This is separate from the checkbox row-selection system. Cell selection is for copying data out of the table. Row checkbox selection is for CRUD operations (edit, delete, export selected).


Visual Anatomy

┌──────┬──────────────────────┬────────────────┬───────────────┐
│ ☐ │ Name ↑ │ Status │ Email │
├──────┼──────────────────────┼────────────────┼───────────────┤
│ ☐ │ Jane Smith │ ● Active │ jane@... │
│ ☐ │[Super Administrator ]│[Active ]│ │ ← selected cells (highlighted)
│ ☐ │ Alice Brown │ ● Active │ alice@... │
└──────┴──────────────────────┴────────────────┴───────────────┘

Selected cells show a highlight (typically bg-primary/10 or a blue tint) to indicate the active selection range.


Selection Behavior

Single Cell Click

Click any data cell (not the checkbox or actions column) to select it as the anchor of a new selection:

Click Jane Smith's Name cell → that cell is selected (highlighted)

Drag to Select Range

Click and drag across cells to select a rectangular range:

Click "Super Administrator" in Name column, drag right to "Active" in Status column
→ Selects both cells in that row

Click "Super Administrator", drag down and right across 3 rows and 2 columns
→ Selects a 3×2 rectangle of cells

The selection count is shown in the context menu header: "Copy Selection (10 cells)".

Keyboard Selection Extension

  • Shift+Click — extends selection from anchor to clicked cell
  • Shift+Arrow — extends selection in the arrow direction
  • Ctrl+A (or Cmd+A) — selects all visible cells in the table

Right-Click Context Menu

When any cells are selected, right-clicking anywhere within the selection opens the Cell Copy Context Menu:

┌─────────────────────────────┐
│ Copy Selection (10 cells) │ ← Only shown when 2+ cells selected
│ Copy Cell │ ← Always shown
│ Copy Cell with Header │ ← Cell value + column header name
│ Copy Row │ ← All visible cell values in the row
│ Copy Column │ ← All visible values in the column
└─────────────────────────────┘
Menu ItemBehaviorExample Output
Copy Selection (N cells)Copies all selected cells as tab-delimited text (ready to paste into Excel)Super Administrator\tActive\nSystem Bootstrap\tActive
Copy CellCopies the single right-clicked cell's valueSuper Administrator
Copy Cell with HeaderCopies "Column Name: Value" formatName: Super Administrator
Copy RowCopies all visible column values in the row, tab-delimitedsuperadmin\tSuper Administrator\t—\tsuperadmin@anshin.com\tActive
Copy ColumnCopies all visible values in the right-clicked column, newline-delimitedSuper Administrator\nSystem Bootstrap\nJane Smith

Context Menu Implementation

// The context menu is a ContextMenu (right-click) from shadcn/ui
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from '@/components/ui/context-menu';

// Wraps the table body
<ContextMenu>
<ContextMenuTrigger asChild>
<div className="overflow-auto rounded-lg border">
<table>...</table>
</div>
</ContextMenuTrigger>
<ContextMenuContent className="w-64">
{selectedCells.length > 1 && (
<ContextMenuItem onClick={handleCopySelection}>
Copy Selection ({selectedCells.length} cells)
</ContextMenuItem>
)}
<ContextMenuItem onClick={handleCopyCell}>
Copy Cell
</ContextMenuItem>
<ContextMenuItem onClick={handleCopyCellWithHeader}>
Copy Cell with Header
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={handleCopyRow}>
Copy Row
</ContextMenuItem>
<ContextMenuItem onClick={handleCopyColumn}>
Copy Column
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>

Cell Selection State

Cell selection requires tracking:

  1. Anchor cell — where selection started (row index + column id)
  2. Active cell — current end of selection (row index + column id)
  3. Selected range — the rectangular set of cells between anchor and active
interface CellSelection {
anchorRow: number;
anchorCol: string;
activeRow: number;
activeCol: string;
}

// Computed from selection:
const selectedCells = useMemo(() => {
const minRow = Math.min(anchorRow, activeRow);
const maxRow = Math.max(anchorRow, activeRow);
const colIds = getColumnRange(anchorCol, activeCol, columnOrder);
return rows.slice(minRow, maxRow + 1).flatMap(row =>
colIds.map(colId => ({ row, colId, value: row.getValue(colId) }))
);
}, [anchorRow, activeRow, anchorCol, activeCol, columnOrder, rows]);

Copy to Clipboard

All copy operations use the Clipboard API:

const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
toast.success('Copied to clipboard');
} catch {
// Fallback for browsers without clipboard API
const el = document.createElement('textarea');
el.value = text;
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
toast.success('Copied to clipboard');
}
};

// Tab-delimited for spreadsheet paste compatibility:
const copySelection = () => {
const rows = groupByRow(selectedCells);
const text = rows.map(row => row.map(c => c.value ?? '').join('\t')).join('\n');
copyToClipboard(text);
};

Tab-delimited output means users can paste directly into Excel/Google Sheets and data lands in correct columns.


Visual Feedback

StateVisual Indicator
Single cell focusedoutline: 2px solid hsl(var(--ring)) or bg-primary/10
Range selectedbg-primary/10 on all cells in range
Copy successfultoast.success('Copied to clipboard') — top-right toast, auto-dismisses in 2s

Selected cells must be visually distinct from row-selection (checkbox select = bg-primary/10 on full row). Recommendation: use a blue tint (bg-blue-500/10) for cell selection vs primary tint for row selection to differentiate visually.


DataTableSortable Integration

Cell selection is enabled via enableCellSelection prop:

<DataTableSortable
table={table}
enableCellSelection={true} // enables drag selection + context menu
onCellContextMenu={handleContextMenu}
/>

When enableCellSelection={false} (default for simpler tables), clicking rows behaves normally for row selection/expansion only.


Relationship with Row Checkbox Selection

These two systems are independent:

SystemPurposeVisualTrigger
Checkbox selectionCRUD operations (Edit, Delete, Export)bg-primary/10 full rowClick checkbox
Cell selectionCopy data to clipboardbg-blue-500/10 rectangleClick-drag on cells

A user can have checkboxes checked on rows 1 and 3 while also having a cell selection on rows 2-4, columns 2-3 — these don't interfere.


Violation Checklist

  • Table body wrapped in ContextMenu trigger
  • Context menu uses shadcn ContextMenu (never custom)
  • "Copy Selection (N cells)" only shown when 2+ cells selected
  • Tab-delimited format for multi-cell copies (Excel-compatible)
  • Copy shows toast.success confirmation
  • Cell selection visual is distinct from row checkbox selection
  • Select/actions columns excluded from cell selection