# DataGridSelectable Guides

## Patterns

### Availability Matrix

When to use:
Build a schedule or capacity grid where the app owns the visual cell treatment.

Composition notes:
Keep cell keys stable and use `disabled` or `exclude` for blocked positions.

Trade-offs:
You get fine-grained control, but also own the visible selected and active states in the cell UI.

```tsx
import { useState } from 'react';
import {
  Button,
  DataGridSelectable,
  DataGridSelectableCell,
  DataGridSelectableRow,
  DataGridSelectableRowGroup,
  Text,
} from '@vega-ui/react';
import './examples.css';

const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'];
const parts = ['Morning', 'Afternoon', 'Evening'];

export const layout = 'wide';

export default function ParentManagedMultiSelect() {
  const [selected, setSelected] = useState<string[]>(['Tue Morning', 'Thu Afternoon']);

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-8)', width: 'fit-content' }}>
      <DataGridSelectable
        aria-label='Interview availability'
        aria-multiselectable='true'
        selection='multiple'
        selected={selected}
        onSelectCell={setSelected}
        defaultActive='Mon Morning'
        className='selectableGrid'
      >
        <DataGridSelectableRowGroup>
          <DataGridSelectableRow row={0}>
            <div role='columnheader'>Time</div>
            {days.map((day) => <div key={day} role='columnheader'>{day}</div>)}
          </DataGridSelectableRow>
        </DataGridSelectableRowGroup>
        <DataGridSelectableRowGroup>
          {parts.map((part, row) => (
            <DataGridSelectableRow key={part} row={row + 1}>
              <div role='rowheader'>{part}</div>
              {days.map((day, col) => (
                <DataGridSelectableCell key={day} col={col} cellKey={`${day} ${part}`} aria-label={`${day} ${part}`} />
              ))}
            </DataGridSelectableRow>
          ))}
        </DataGridSelectableRowGroup>
      </DataGridSelectable>
      <Text size={2} style={{ color: 'var(--label-secondary)', contain: 'inline-size' }}>
        {selected.length === 0 ? 'No slots picked yet' : `${selected.length} slots: ${selected.join(', ')}`}
      </Text>
      <Button size='sm' variant='secondary' appearance='ghost' disabled={selected.length === 0} style={{ justifySelf: 'start' }} onClick={() => setSelected([])}>
        Clear
      </Button>
    </div>
  );
}
```

### Custom Range Grid

When to use:
Range selection should follow domain-specific ordering instead of simple matrix order.

Composition notes:
Provide `compare` and `resolveRange` so expansion matches the product model. In the example a maintenance window runs down a day's column before moving to the next day: `compare` orders keys by day, then by hour, and `resolveRange` lists every block in that order.

Trade-offs:
The behavior becomes powerful, but debugging range edges is more involved.

```tsx
import { useState } from 'react';
import {
  DataGridSelectable,
  DataGridSelectableCell,
  DataGridSelectableRow,
  DataGridSelectableRowGroup,
  Text,
  type DataGridResolveValue,
} from '@vega-ui/react';
import './examples.css';

const days = ['Mon', 'Tue', 'Wed'];
const hours = ['09:00', '11:00', '13:00', '15:00', '17:00'];
const slots = days.flatMap((day) => hours.map((hour) => `${day} ${hour}`));

const compareSlots = (a: string, b: string): -1 | 0 | 1 => {
  const difference = slots.indexOf(a) - slots.indexOf(b);

  if (difference < 0) return -1;
  if (difference > 0) return 1;

  return 0;
};

const resolveSlots = (start: DataGridResolveValue<string>, end: DataGridResolveValue<string>) => {
  const from = slots.indexOf(start.key);
  const to = slots.indexOf(end.key);

  return slots.slice(Math.min(from, to), Math.max(from, to) + 1);
};

const describeWindow = (blocks: string[]) => {
  if (blocks.length < 2) return 'Pick where the window starts and ends.';

  return `${blocks.length} two-hour blocks, ${blocks[0]} → ${blocks[blocks.length - 1]}`;
};

export default function CustomRangeGrid() {
  const [maintenance, setMaintenance] = useState<string[]>([]);

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-8)', width: 'fit-content' }}>
      <DataGridSelectable
        aria-label='Maintenance window'
        selection='range'
        selected={maintenance}
        onSelectCell={setMaintenance}
        compare={compareSlots}
        resolveRange={resolveSlots}
        defaultActive='Mon 09:00'
        className='selectableGrid'
      >
        <DataGridSelectableRowGroup>
          <DataGridSelectableRow row={0}>
            <div role='columnheader'>Time</div>
            {days.map((day) => <div key={day} role='columnheader'>{day}</div>)}
          </DataGridSelectableRow>
        </DataGridSelectableRowGroup>
        <DataGridSelectableRowGroup>
          {hours.map((hour, row) => (
            <DataGridSelectableRow key={hour} row={row + 1}>
              <div role='rowheader'>{hour}</div>
              {days.map((day, col) => (
                <DataGridSelectableCell key={day} col={col} cellKey={`${day} ${hour}`} aria-label={`${day} ${hour}`} />
              ))}
            </DataGridSelectableRow>
          ))}
        </DataGridSelectableRowGroup>
      </DataGridSelectable>
      <Text size={2} style={{ color: 'var(--label-secondary)', contain: 'inline-size' }}>{describeWindow(maintenance)}</Text>
    </div>
  );
}
```

## Comparison

### Quick Decision Rule

Choose `DataGridSelectable` when selection behavior is the main requirement and the visual cell treatment is custom. Choose `DataGridPicker` when the visual picker layer should be standardized.

### `DataGridSelectable` vs `DataGrid`

Use `DataGridSelectable` when cells must be selectable.

Use `DataGrid` when the grid is navigable or presentational but not selection-driven.

### `DataGridSelectable` vs `DataGridPicker`

Use `DataGridSelectable` when the product needs custom cell styling and direct control over the visual layer.

Use `DataGridPicker` when a picker-style selection surface should come from VegaUI.

### Choose This Component When

- range, multi-select, or custom selection rules are the main concern
- the visual layer is application-specific
- custom comparators or range resolution are required

### Do Not Choose This Component When

- the UI just needs a standard picker grid
- consumers should not have to design the visual selection layer themselves
- the grid is purely presentational

## Troubleshooting

### Range Selection Expands Unexpectedly

#### Symptom

Shift-key or pointer expansion selects cells outside the expected range.

#### Likely Cause

The key ordering model does not match the domain, or `resolveRange` is missing for a custom range shape.

#### How To Verify

- inspect the actual cell keys
- test range expansion with a minimal grid
- compare expected order with `compare`

#### Fix

Provide stable keys and a custom `compare` or `resolveRange` when the default ordering is not enough.

### Disabled Cells Still Affect Navigation

#### Symptom

Focus or selection still moves through disabled cells.

#### Likely Cause

`excludeDisabled` is not enabled, or the disabled map does not match the real keys.

#### How To Verify

- inspect the `disabled` input
- test the same grid with `excludeDisabled`

#### Fix

Pass the correct disabled keys and enable `excludeDisabled` when blocked cells should be skipped.

### Selection Looks Broken After Reordering Rows

#### Symptom

Previously selected or active cells drift after data changes.

#### Likely Cause

Cell keys are not stable across renders.

#### How To Verify

- log generated keys before and after the data update
- confirm whether the same logical cell keeps the same key

#### Fix

Use stable domain keys instead of render-order-derived identifiers when possible.
