# Selection Plugin

> Cell, row, and range selection with full keyboard support, conditional selection, and checkbox mode.

The Selection plugin adds cell, row, and range selection capabilities to the grid with full keyboard support. Whether you need simple cell highlighting or complex multi-range selections, this plugin has you covered.

## Do you actually need this plugin?

The Selection plugin exists to maintain a **selection state** — a set of cells, rows, or ranges — that other plugins can act on. It is the prerequisite for things like:

- [ClipboardPlugin](/grid/plugins/clipboard.md) — copy/paste the current selection
- [ExportPlugin](/grid/plugins/export.md) — when `onlySelected: true`, export only the rows the user has selected (without it, all rows are exported)
- [ContextMenuPlugin](/grid/plugins/context-menu.md) — when present, the context menu will operate on the **multi-row** selection instead of just the right-clicked row (the menu itself works fine without it)
- Any custom logic that needs to know "which rows/cells did the user pick?"

If all you want is to **visually highlight the row the user has navigated to** (the focused row), you do **not** need this plugin. The grid core already tracks keyboard focus on the active cell via the `.cell-focus` class — you can style the surrounding row with a small CSS snippet:

```css
/* Highlight the row containing the focused cell, no plugin required */
tbw-grid .data-grid-row:has(.cell-focus) {
  background-color: var(--tbw-focus-background, rgba(from var(--tbw-color-accent) r g b / 12%));
}

/* Optional: paint the focus tint over sticky/pinned cells too,
   which otherwise have an opaque background to mask scrolling content */
tbw-grid .data-grid-row:has(.cell-focus) > .cell.sticky-left,
tbw-grid .data-grid-row:has(.cell-focus) > .cell.sticky-right {
  background:
    linear-gradient(var(--tbw-focus-background, rgba(from var(--tbw-color-accent) r g b / 12%)) 0 0),
    var(--tbw-color-panel-bg);
}

/* Optional: suppress the per-cell focus outline if you want a row-only
   indicator. The grid core paints `outline: var(--tbw-focus-outline)` on
   `.cell-focus` while the grid has focus; leaving it ON makes it easier
   for keyboard users to see which cell they're on inside the highlighted
   row. Only add this rule if you specifically want the row to be the sole
   focus indicator (mimicking the Selection plugin's row mode).

   The grid core sets the `data-has-focus` attribute on the host element
   whenever focus is inside the grid (managed by the focus controller),
   and removes it on blur — so this rule only suppresses the cell outline
   while the grid is actively focused. */
tbw-grid[data-has-focus] .cell-focus {
  outline: none;
}
```

This mirrors what the Selection plugin does for `row` mode focus — without shipping the plugin's selection-state machinery, keyboard range extension, or click handlers. Reach for the plugin only when something downstream needs to read or react to a selection.

## Installation

```ts
import '@toolbox-web/grid/features/selection';
```

## Basic Usage

#### TypeScript

  ```ts
  import { queryGrid } from '@toolbox-web/grid';
  import '@toolbox-web/grid/features/selection';

  const data = [
    { id: 1, name: 'Alice', email: 'alice@example.com' },
    { id: 2, name: 'Bob',   email: 'bob@example.com' },
    { id: 3, name: 'Carol', email: 'carol@example.com' },
  ];

  const grid = queryGrid('tbw-grid');
  grid.gridConfig = {
    columns: [
      { field: 'id', header: 'ID' },
      { field: 'name', header: 'Name' },
      { field: 'email', header: 'Email' },
    ],
    features: {
      selection: 'row',
    },
  };
  grid.rows = data;
  ```

#### React

  ```tsx
  import '@toolbox-web/grid-react/features/selection';
  import { DataGrid } from '@toolbox-web/grid-react';

  function MyGrid({ data }) {
    return (
      <DataGrid
        rows={data}
        columns={[
          { field: 'id', header: 'ID' },
          { field: 'name', header: 'Name' },
          { field: 'email', header: 'Email' },
        ]}
        selection={{ mode: 'row' }}
        style={{ height: '400px' }}
      />
    );
  }
  ```

#### Vue

  ```html
  <script setup>
  import '@toolbox-web/grid-vue/features/selection';
  import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';
  const data = [
    { id: 1, name: 'Alice', email: 'alice@example.com' },
    { id: 2, name: 'Bob', email: 'bob@example.com' },
  ];
  </script>

  <template>
    <TbwGrid :rows="data" selection="row" style="height: 400px">
      <TbwGridColumn field="id" header="ID" />
      <TbwGridColumn field="name" header="Name" />
      <TbwGridColumn field="email" header="Email" />
    </TbwGrid>
  </template>
  ```

#### Angular

  ```typescript
  import { GridSelectionDirective } from '@toolbox-web/grid-angular/features/selection';
  import { Component } from '@angular/core';
  import { Grid } from '@toolbox-web/grid-angular';
  import type { ColumnConfig } from '@toolbox-web/grid';

  @Component({
    selector: 'app-my-grid',
    imports: [Grid, GridSelectionDirective],
    template: `
      <tbw-grid
        [rows]="rows"
        [columns]="columns"
        [selection]="'row'"
        style="height: 400px; display: block;">
      </tbw-grid>
    `,
  })
  export class MyGridComponent {
    rows = [
      { id: 1, name: 'Alice', email: 'alice@example.com' },
      { id: 2, name: 'Bob', email: 'bob@example.com' },
    ];
    columns: ColumnConfig[] = [
      { field: 'id', header: 'ID' },
      { field: 'name', header: 'Name' },
      { field: 'email', header: 'Email' },
    ];
  }
  ```

## Interactive Demo

Switch between selection modes to see how each one behaves. The state panel below the grid shows the current selection in real time.

- **Cell mode:** Click cells to select them individually
- **Row mode:** Click anywhere in a row to select the entire row. Ctrl+Click to toggle, Shift+Click for range
- **Range mode:** Click and drag to select rectangular ranges. Ctrl+drag for multiple ranges
- **Column mode:** Ctrl/⌘+Click on a header (or Ctrl/⌘+Space on a focused cell) selects that column. Ctrl+Shift+Click extends from the column anchor.

```ts
// SelectionPlaygroundDemo.astro
  import '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/selection';

  const container = document.getElementById('playground-selection-demo');
  if (container) {
    const grid = queryGrid('tbw-grid', container);
    const output = container.querySelector<HTMLElement>('[data-output-id="selection-demo"]');

    if (grid) {
      const sampleData = [
        { id: 1, name: 'Alice', department: 'Engineering', salary: 95000, email: 'alice@example.com' },
        { id: 2, name: 'Bob', department: 'Marketing', salary: 75000, email: 'bob@example.com' },
        { id: 3, name: 'Carol', department: 'Engineering', salary: 105000, email: 'carol@example.com' },
        { id: 4, name: 'Dan', department: 'Sales', salary: 85000, email: 'dan@example.com' },
        { id: 5, name: 'Eve', department: 'Marketing', salary: 72000, email: 'eve@example.com' },
        { id: 6, name: 'Frank', department: 'Engineering', salary: 98000, email: 'frank@example.com' },
        { id: 7, name: 'Grace', department: 'Sales', salary: 88000, email: 'grace@example.com' },
        { id: 8, name: 'Henry', department: 'HR', salary: 65000, email: 'henry@example.com' },
      ];

      const columns = [
        { field: 'id', header: 'ID', type: 'number' as const },
        { field: 'name', header: 'Name' },
        { field: 'department', header: 'Department' },
        { field: 'salary', header: 'Salary', type: 'number' as const },
        { field: 'email', header: 'Email' },
      ];

      function setupGrid(mode: string) {
        const selectionMode: any = mode === 'row+column' ? ['row', 'column'] : mode;
        grid.gridConfig = { columns, features: { selection: { mode: selectionMode } } };
        grid.rows = sampleData;

        grid.on('selection-change', () => {
          const plugin = grid.getPluginByName('selection');
          if (!plugin || !output) return;
          const sel: any = plugin.getSelection();
          const modeLabel = Array.isArray(sel.mode) ? '[' + sel.mode.join(', ') + ']' : sel.mode;
          const lines = ['<b>mode:</b> ' + modeLabel, '<b>activeAxis:</b> ' + sel.activeAxis];

          if (sel.activeAxis === 'column') {
            const cols = plugin.getSelectedColumns();
            lines.push('<b>selectedColumns:</b> [' + cols.map((c: string) => '"' + c + '"').join(', ') + ']');
          }

          if (sel.activeAxis === 'row') {
            const indices = plugin.getSelectedRowIndices();
            lines.push('<b>selectedRows:</b> [' + indices.join(', ') + ']');
            if (indices.length > 0) {
              const names = indices.map((i: number) => sampleData[i]?.name).filter(Boolean);
              lines.push('<b>rowData:</b> ' + names.map((n: string) => '"' + n + '"').join(', '));
            }
          }

          if (sel.ranges.length > 0) {
            const rangeStrs = sel.ranges.map(
              (r: { from: { row: number; col: number }; to: { row: number; col: number } }) =>
                '{ from: {row:' + r.from.row + ', col:' + r.from.col + '}, to: {row:' + r.to.row + ', col:' + r.to.col + '} }'
            );
            lines.push('<b>ranges:</b> [' + rangeStrs.join(', ') + ']');
            if (sel.mode === 'range') {
              lines.push('<b>cellCount:</b> ' + plugin.getSelectedCells().length);
            }
          } else {
            lines.push('<b>ranges:</b> []');
          }

          output.innerHTML = lines.join('<br>');
        });
      }

      setupGrid('cell');

      container.addEventListener('control-change', ((e: CustomEvent) => {
        setupGrid(e.detail.allValues.mode as string);
      }) as EventListener);
    }
  }
```

### Checkbox Selection

Add `checkbox: true` to show a selection checkbox column with a "select all" header. Works exclusively in row mode.

```ts
// SelectionCheckboxDemo.astro
  import '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/selection';

  const container = document.getElementById('checkbox-selection-demo');
  if (container) {
    const grid = queryGrid('tbw-grid', container);
    if (grid) {
      grid.gridConfig = {
        columns: [
          { field: 'id', header: 'ID', type: 'number' as const },
          { field: 'name', header: 'Name' },
          { field: 'department', header: 'Department' },
          { field: 'salary', header: 'Salary', type: 'number' as const },
        ],
        features: { selection: { mode: 'row', checkbox: true } },
      };
      grid.rows = [
        { id: 1, name: 'Alice', department: 'Engineering', salary: 95000 },
        { id: 2, name: 'Bob', department: 'Marketing', salary: 75000 },
        { id: 3, name: 'Carol', department: 'Engineering', salary: 105000 },
        { id: 4, name: 'Dan', department: 'Sales', salary: 85000 },
        { id: 5, name: 'Eve', department: 'Marketing', salary: 72000 },
      ];
    }
  }
```

```ts
features: { selection: { mode: 'row', checkbox: true } }
```

### Column Selection

(Since 2.8.0)

Set `mode: 'column'` to enable column-axis selection. Selected columns are tracked by **field name** (not visible-index), so the selection survives column pinning, reordering, virtualization recycling, and visibility changes.

```ts
features: { selection: { mode: 'column' } }
```

You can combine column selection with one in-row axis (`'cell'`, `'row'`, or `'range'`) by passing an array. The two axes are **mutually exclusive** at any given moment — selecting on one clears the other and announces the axis change to assistive technology:

```ts
features: { selection: { mode: ['row', 'column'] } }
```

Invalid combinations (`['cell', 'row']`, `['cell', 'range']`, `['row', 'range']`) throw at attach time — only `'column' + X` array shapes are accepted.

**Activation paths**

| Input | Action |
| ----- | ------ |
| `Ctrl/⌘ + Click` on column header | Toggle column |
| `Ctrl/⌘ + Shift + Click` on column header | Extend from anchor |
| `Ctrl/⌘ + Space` | Toggle column at focused cell |
| `Ctrl/⌘ + Shift + ←/→` | Extend column selection |
| Plain header click | Reserved for sort (no selection change) |

Utility columns (checkbox, expander, etc.) are never selectable. With `multiSelect: false`, only one column can be selected at a time and `selectAllColumns()` is a no-op.

## Configuration Options

### Grid-Level Toggle

You can disable selection grid-wide using `gridConfig.selectable`:

```ts
grid.gridConfig = {
  selectable: false, // Disables ALL selection
  features: { selection: 'range' },
};
```

### Plugin Options

See [`SelectionConfig`](./Interfaces/SelectionConfig/) for the full list of options and defaults.

### Double-Click Selection Trigger

In data-entry grids, you may want single-click to only **focus** the row/cell for keyboard navigation, while **double-click** changes the selection state.

:::note
`triggerOn` only applies to `'cell'` and `'row'` modes. Range mode uses drag selection (mousedown → mousemove), which is unaffected.
:::

```ts
features: {
  selection: {
    mode: 'row',
    triggerOn: 'dblclick',  // Single-click focuses, double-click selects
  },
}
```

### Conditional Selection

Use the `isSelectable` callback to prevent selection of specific rows or cells:

```ts
features: {
  selection: {
    mode: 'row',
    isSelectable: (row) => row.status !== 'locked',
  },
}
```

**Behavior of non-selectable rows/cells:**

| Aspect | Behavior |
|--------|----------|
| Click | Ignored (no selection change) |
| Keyboard | Skipped with Shift+Arrow |
| Select All | Excluded |
| Visual | Muted via `[data-selectable="false"]` attribute |
| Focus | Still navigable |

```ts
// ConditionalSelectionDemo.astro
  import '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/selection';

  const grid = queryGrid('#demo-conditional-selection');
  if (grid) {
    grid.gridConfig = {
      columns: [
        { field: 'id', header: 'ID', type: 'number', width: 80 },
        { field: 'name', header: 'Name' },
        { field: 'department', header: 'Department' },
        { field: 'status', header: 'Status' },
      ],
      features: {
        selection: {
          mode: 'row',
          isSelectable: (row: { status: string }) => row.status !== 'locked',
        },
      },
    };

    grid.rows = [
      { id: 1, name: 'Alice Johnson', department: 'Engineering', status: 'active' },
      { id: 2, name: 'Bob Smith', department: 'Marketing', status: 'locked' },
      { id: 3, name: 'Carol Davis', department: 'Engineering', status: 'active' },
      { id: 4, name: 'Dan Wilson', department: 'Sales', status: 'locked' },
      { id: 5, name: 'Eve Brown', department: 'HR', status: 'active' },
      { id: 6, name: 'Frank Miller', department: 'Engineering', status: 'active' },
      { id: 7, name: 'Grace Lee', department: 'Finance', status: 'locked' },
      { id: 8, name: 'Hank Taylor', department: 'Sales', status: 'active' },
    ];
  }
```

## Keyboard Shortcuts

| Shortcut | Action |
| -------- | ------ |
| `Arrow Keys` | Move focus |
| `Shift + Arrow` | Extend selection (row and range modes) |
| `Shift + Page Up/Down` | Extend selection by page (row and range modes) |
| `Shift + Ctrl/⌘ + Home/End` | Extend selection to first/last row (row and range modes) |
| `Ctrl/⌘ + Click` | Toggle row/cell (multi-select) |
| `Shift + Click` | Extend selection from anchor |
| `Ctrl/⌘ + A` | Select all (row and range modes) |
| `Ctrl/⌘ + Click` (header) | Toggle column selection (column mode) |
| `Ctrl/⌘ + Shift + Click` (header) | Extend column selection from anchor |
| `Ctrl/⌘ + Space` | Toggle column at the focused column (column mode) |
| `Ctrl/⌘ + Shift + ←/→` | Extend column selection (column mode) |
| `Escape` | Clear current selection (whichever axis is active) |

## Programmatic API

```ts
const plugin = grid.getPluginByName('selection');

// Query
const selection = plugin.getSelection();
plugin.isCellSelected(2, 1);

// Row mode
plugin.selectRows([0, 2, 4]);
const rows = plugin.getSelectedRows<Employee>();

// Range mode
plugin.setRanges([{ from: { row: 0, col: 0 }, to: { row: 5, col: 3 } }]);

// Column mode (since 2.8.0)
plugin.selectColumn('email');
plugin.selectColumn('name', { toggle: true });
plugin.selectColumn('lastLogin', { range: true }); // From column anchor
plugin.deselectColumn('email');
plugin.selectAllColumns();
plugin.clearColumnSelection();
const cols = plugin.getSelectedColumns(); // string[]

// Actions
plugin.selectAll();
plugin.clearSelection();
```

:::tip
Use `getSelectedRows()` to retrieve actual row data objects rather than resolving indices manually. Row indices refer to positions in the grid's current (sorted/filtered) row array, which may differ from your original data array.
:::

## Events

| Event | Description |
| ----- | ----------- |
| `selection-change` | Fired when the selection is modified |

```ts
// SelectionEventsDemo.astro
import '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';

import '@toolbox-web/grid/features/selection';

const container = document.getElementById('selection-events-demo');
if (container) {
  const grid = queryGrid('tbw-grid', container);
  const logEl = container.querySelector('#selection-events-log')!;
  const clearBtn = container.querySelector('#selection-events-clear-btn');

  grid.gridConfig = {
    columns: [
      { field: 'id', header: 'ID', type: 'number', width: 60 },
      { field: 'name', header: 'Name' },
      { field: 'department', header: 'Department' },
      { field: 'salary', header: 'Salary', type: 'number', align: 'right' },
    ],
    features: { selection: 'range' },
  };

  grid.rows = [
    { id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 85000 },
    { id: 2, name: 'Bob Smith', department: 'Marketing', salary: 72000 },
    { id: 3, name: 'Carol White', department: 'Sales', salary: 68000 },
    { id: 4, name: 'David Brown', department: 'Engineering', salary: 92000 },
    { id: 5, name: 'Eve Davis', department: 'HR', salary: 65000 },
  ];

  function addLog(msg: string) {
    const entry = document.createElement('div');
    entry.className = 'event-log-entry';
    entry.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
    logEl.prepend(entry);
  }

  grid.on('selection-change', (detail) => {
    addLog(`selection-change — ${detail.selectedCount ?? 0} cell(s) selected`);
  });

  grid.on('row-select', (detail) => {
    addLog(`row-select — row ${detail.rowIndex}`);
  });

  clearBtn?.addEventListener('click', () => {
    logEl.innerHTML = '';
  });
}
```

### `selection-change` Detail

See [`SelectionChangeDetail`](./Interfaces/SelectionChangeDetail/) and [`CellRange`](./Interfaces/CellRange/) for the full event payload types.

## Styling

:::tip
If you only want a focused-row highlight and don't need the selection state, see [Do you actually need this plugin?](#do-you-actually-need-this-plugin) for a CSS-only recipe that mimics the row-focus look.
:::

### CSS Custom Properties

| Property | Default | Description |
| --- | --- | --- |
| `--tbw-focus-background` | `rgba(accent, 12%)` | Focused row background |
| `--tbw-range-selection-bg` | `rgba(accent, 12%)` | Range selection fill |
| `--tbw-range-border-color` | `var(--tbw-color-accent)` | Range selection border |
| `--tbw-color-accent` | `#3b82f6` | Primary accent color |

```css
tbw-grid {
  --tbw-range-selection-bg: rgba(76, 175, 80, 0.15);
  --tbw-range-border-color: #4caf50;
  --tbw-focus-background: rgba(76, 175, 80, 0.1);
}
```

### CSS Classes

| Class | Element |
| --- | --- |
| `.selecting` | Grid during range drag |
| `.row-focus` | Focused row (row mode) |
| `.cell-focus` | Focused cell (cell mode) |
| `.selected` | Selected cell in range |
| `.selected.top` / `.bottom` / `.first` / `.last` | Range boundary edges |

## Works Well With

| Plugin | Integration |
|--------|-------------|
| [EditingPlugin](/grid/plugins/editing.md) | Click-to-select + double-click-to-edit. In `mode: 'row'` the row entering edit is auto-added to the selection so `getSelectedRows()` always reflects the row the user is visibly editing. With `multiSelect: false` the selection is replaced; otherwise the edited row is added to the existing set. Selection is also automatically cleared when the host replaces the `rows` array with a different number of source rows, preventing stale indices from pointing at the wrong rows. |
| [ClipboardPlugin](/grid/plugins/clipboard.md) | Copy/paste selected cells (requires SelectionPlugin) |
| [FilteringPlugin](/grid/plugins/filtering.md) | Filter data, then select from results |
| [ContextMenuPlugin](/grid/plugins/context-menu.md) | Right-click selected rows for actions |

## See Also

- [Common Patterns](/grid/guides/common-patterns.md) — Full application recipes using selection
- [Plugins Overview](/grid/plugins.md) — All available plugins and compatibility
- [Events Reference](/grid/api-reference.md#events) — `selection-change` event details
