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:
- Row selection — checkbox column (for toolbar actions like Edit, Delete)
- Cell selection — click a single cell to focus it
- Range selection — click-drag across multiple cells in multiple rows/columns
- 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 cellShift+Arrow— extends selection in the arrow directionCtrl+A(orCmd+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 Item Specs
| Menu Item | Behavior | Example 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 Cell | Copies the single right-clicked cell's value | Super Administrator |
| Copy Cell with Header | Copies "Column Name: Value" format | Name: Super Administrator |
| Copy Row | Copies all visible column values in the row, tab-delimited | superadmin\tSuper Administrator\t—\tsuperadmin@anshin.com\tActive |
| Copy Column | Copies all visible values in the right-clicked column, newline-delimited | Super 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:
- Anchor cell — where selection started (row index + column id)
- Active cell — current end of selection (row index + column id)
- 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
| State | Visual Indicator |
|---|---|
| Single cell focused | outline: 2px solid hsl(var(--ring)) or bg-primary/10 |
| Range selected | bg-primary/10 on all cells in range |
| Copy successful | toast.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:
| System | Purpose | Visual | Trigger |
|---|---|---|---|
| Checkbox selection | CRUD operations (Edit, Delete, Export) | bg-primary/10 full row | Click checkbox |
| Cell selection | Copy data to clipboard | bg-blue-500/10 rectangle | Click-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
ContextMenutrigger - 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.successconfirmation - Cell selection visual is distinct from row checkbox selection
- Select/actions columns excluded from cell selection