# DataGridPicker

## Summary

`DataGridPicker` is the visual picker layer built on top of `DataGridSelectable`. It keeps the same selection engine while adding shared picker size and variant semantics for dense grids such as dates, months, years, and custom matrices.

## Imports

```tsx
import {
  DataGridPicker,
  DataGridPickerItem,
  DataGridPickerRow,
  DataGridPickerRowGroup,
  DataGridPickerScroller,
  DataGridPickerScrollerContent,
  type DataGridPickerItemProps,
  type DataGridPickerProps,
  type DataGridPickerRowGroupProps,
  type DataGridPickerRowProps,
  type DataGridPickerScrollerContentProps,
  type DataGridPickerScrollerProps,
} from '@vega-ui/react';
```

## Exported Types

- `DataGridPickerProps<K, S>`
- `DataGridPickerItemProps`
- `DataGridPickerRowProps`
- `DataGridPickerRowGroupProps`
- `DataGridPickerScrollerProps`
- `DataGridPickerScrollerContentProps`

## Minimal Composition

```tsx
import {
  DataGridPicker,
  DataGridPickerItem,
  DataGridPickerRow,
  DataGridPickerRowGroup,
} from '@vega-ui/react';

export default function MinimalComposition() {
  return (
    <DataGridPicker aria-label='Plan length' defaultActive='1m' style={{ width: 240 }}>
      <DataGridPickerRowGroup>
        <DataGridPickerRow row={0}>
          <DataGridPickerItem col={0} value='1m'>1 mo</DataGridPickerItem>
          <DataGridPickerItem col={1} value='6m'>6 mo</DataGridPickerItem>
          <DataGridPickerItem col={2} value='12m'>12 mo</DataGridPickerItem>
        </DataGridPickerRow>
      </DataGridPickerRowGroup>
    </DataGridPicker>
  );
}
```

## Required Parts

- `DataGridPicker`: root picker behavior plus size and variant context
- `DataGridPickerRowGroup`
- `DataGridPickerRow`
- `DataGridPickerItem`

## Optional Parts

- `DataGridPickerScroller`
- `DataGridPickerScrollerContent`

## Composition Order

1. `DataGridPicker`
2. optional `DataGridPickerScroller`
3. `DataGridPickerScrollerContent` when scrolling is used
4. `DataGridPickerRowGroup`
5. `DataGridPickerRow`
6. `DataGridPickerItem`

## Variants

- Variant: `primary`, `secondary`
- Size: `xs`, `sm`, `md`, `lg`, `xl`
- Selection: `single`, `multiple`, `range`
- Layout: static grid or scroller-based paging

## Edge Cases

- Verify active, focused, and selected states independently.
- Scrollable pickers should preserve keyboard navigation and visible focus.
- Scoped row groups matter in paged scroller compositions.

## Common Mistakes

- Treating picker items as plain layout nodes without selection semantics.
- Forgetting to test scroll and keyboard interaction together.
- Rebuilding item styling manually when the picker layer already defines the intended visual semantics.

## Examples

### Basic

#### Basic: statement month

Twelve months in a 4×3 matrix with single selection. `value` on every `DataGridPickerItem` is its key, so `defaultSelected` and `defaultActive` use the same `2026-05` strings; the picker supplies the selected, hover and focus styles.

```tsx
import {
  DataGridPicker,
  DataGridPickerItem,
  DataGridPickerRow,
  DataGridPickerRowGroup,
} from '@vega-ui/react';

const months = [
  ['Jan', 'Feb', 'Mar', 'Apr'],
  ['May', 'Jun', 'Jul', 'Aug'],
  ['Sep', 'Oct', 'Nov', 'Dec'],
];

export default function CompactMonthLikeMatrix() {
  return (
    <DataGridPicker
      aria-label='Statement month, 2026'
      size='sm'
      defaultSelected='2026-05'
      defaultActive='2026-05'
      style={{ width: 280 }}
    >
      <DataGridPickerRowGroup>
        {months.map((row, rowIndex) => (
          <DataGridPickerRow key={rowIndex} row={rowIndex}>
            {row.map((month, col) => {
              const value = `2026-${String(rowIndex * 4 + col + 1).padStart(2, '0')}`;

              return (
                <DataGridPickerItem key={month} col={col} value={value}>{month}</DataGridPickerItem>
              );
            })}
          </DataGridPickerRow>
        ))}
      </DataGridPickerRowGroup>
    </DataGridPicker>
  );
}
```

### Controlled/Stateful

#### Controlled/Stateful: campaign months as a range

`selection='range'` with the range owned by the parent. Items between the ends get `data-range` styling automatically, the summary reads the same array, and "Clear" resets it. Keys are `YYYY-MM` strings, which already sort in calendar order.

```tsx
import { useState } from 'react';
import {
  Button,
  DataGridPicker,
  DataGridPickerItem,
  DataGridPickerRow,
  DataGridPickerRowGroup,
  Text,
} from '@vega-ui/react';

const names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const rows = [0, 1, 2];
const cols = [0, 1, 2, 3];

const toValue = (month: number) => `2026-${String(month + 1).padStart(2, '0')}`;
const toName = (value: string) => names[Number(value.slice(5)) - 1];

const describeRange = (range: string[]) => {
  if (range.length === 0) return 'Pick the first month of the campaign.';
  if (range.length === 1) return `Starts in ${toName(range[0])}. Now pick the last month.`;

  const [start, end] = [...range].sort();
  const months = Number(end.slice(5)) - Number(start.slice(5)) + 1;

  return `${toName(start)} – ${toName(end)} 2026, ${months} months`;
};

export default function ControlledRangePickerGrid() {
  const [range, setRange] = useState<string[]>(['2026-03', '2026-07']);

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%', maxWidth: 280 }}>
      <DataGridPicker
        aria-label='Campaign months, 2026'
        selection='range'
        size='sm'
        selected={range}
        onSelectCell={setRange}
        defaultActive='2026-03'
        style={{ width: '100%' }}
      >
        <DataGridPickerRowGroup>
          {rows.map((row) => (
            <DataGridPickerRow key={row} row={row}>
              {cols.map((col) => {
                const value = toValue(row * cols.length + col);

                return (
                  <DataGridPickerItem key={value} col={col} value={value}>{toName(value)}</DataGridPickerItem>
                );
              })}
            </DataGridPickerRow>
          ))}
        </DataGridPickerRowGroup>
      </DataGridPicker>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--spacing-4)' }}>
        <Text size={2} style={{ color: 'var(--label-secondary)', fontVariantNumeric: 'tabular-nums' }}>{describeRange(range)}</Text>
        <Button size='sm' variant='secondary' appearance='ghost' onClick={() => setRange([])}>Clear</Button>
      </div>
    </div>
  );
}
```

### Form/Integration

#### Form/Integration: invoice period in a form

The selected quarter goes into a hidden input and is read with `FormData` on submit; the reset button restores the initial quarter through `onReset`. Q4 is disabled, and the hint explaining why is linked with `aria-describedby`.

```tsx
import { FormEvent, useState } from 'react';
import {
  Button,
  DataGridPicker,
  DataGridPickerItem,
  DataGridPickerRow,
  DataGridPickerRowGroup,
  HelperText,
  Text,
} from '@vega-ui/react';

const quarters = ['2026-Q1', '2026-Q2', '2026-Q3', '2026-Q4'];
const initialQuarter = '2026-Q2';

export default function QuarterSelectorInABillingFlow() {
  const [quarter, setQuarter] = useState(initialQuarter);
  const [result, setResult] = useState('');

  const onSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();

    const period = new FormData(event.currentTarget).get('period');
    setResult(`Exporting invoices for ${period}`);
  };

  const onReset = () => {
    setQuarter(initialQuarter);
    setResult('');
  };

  return (
    <form onSubmit={onSubmit} onReset={onReset} style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%', maxWidth: 280 }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Text id='invoice-period-label' fontWeight={500}>Invoice period</Text>
        <DataGridPicker
          aria-labelledby='invoice-period-label'
          aria-describedby='invoice-period-hint'
          selected={quarter}
          onSelectCell={setQuarter}
          disabled='2026-Q4'
          defaultActive={initialQuarter}
          style={{ width: '100%' }}
        >
          <DataGridPickerRowGroup>
            <DataGridPickerRow row={0}>
              {quarters.map((value, col) => (
                <DataGridPickerItem key={value} col={col} value={value}>{value.slice(5)}</DataGridPickerItem>
              ))}
            </DataGridPickerRow>
          </DataGridPickerRowGroup>
        </DataGridPicker>
        <input type='hidden' name='period' value={quarter} />
        <HelperText id='invoice-period-hint'>Q4 is still open and cannot be invoiced yet.</HelperText>
      </div>
      <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 'var(--spacing-4)' }}>
        <Button type='reset' variant='secondary' appearance='ghost'>Reset</Button>
        <Button type='submit'>Export invoices</Button>
      </div>
      {result && <Text size={2} style={{ color: 'var(--label-secondary)' }}>{result}</Text>}
    </form>
  );
}
```

### Layout/Overlay

#### Layout/Overlay: paged picker with own buttons

The scroller exposes `prev()` and `next()` through `apiRef` for buttons outside the grid. Each page reads its `index` from `useIndexedSnapScrollerContext`, derives its years and unique row numbers from it, and passes it as `scope` of the row group, so focus moves to the first available cell of the page in view. `onScrollSnapChanging` keeps the period label in sync.

```tsx
import { useRef, useState } from 'react';
import { ChevronLeft, ChevronRight } from '@vega-ui/icons';
import {
  DataGridPicker,
  DataGridPickerItem,
  DataGridPickerRow,
  DataGridPickerRowGroup,
  DataGridPickerScroller,
  DataGridPickerScrollerContent,
  Icon,
  IconButton,
  Text,
  useIndexedSnapScrollerContext,
  type IndexedSnapScrollerApiRef,
} from '@vega-ui/react';

const firstYear = 2016;
const rows = [0, 1, 2];
const cols = [0, 1, 2, 3];
const perPage = rows.length * cols.length;

const getPageStart = (page: number) => firstYear + page * perPage;

function YearPage() {
  const { index } = useIndexedSnapScrollerContext();

  return (
    <DataGridPickerRowGroup scope={index}>
      {rows.map((rowIndex) => {
        const row = index * rows.length + rowIndex;

        return (
          <DataGridPickerRow key={row} row={row}>
            {cols.map((col) => {
              const year = getPageStart(index) + rowIndex * cols.length + col;

              return <DataGridPickerItem key={year} col={col} value={year}>{year}</DataGridPickerItem>;
            })}
          </DataGridPickerRow>
        );
      })}
    </DataGridPickerRowGroup>
  );
}

export default function PagedPickerWithOwnButtons() {
  const scrollerRef = useRef<IndexedSnapScrollerApiRef>(null);
  const [page, setPage] = useState(0);

  return (
    <DataGridPicker aria-label='Graduation year' size='sm' defaultSelected={2021} defaultActive={2021} style={{ width: 280 }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
        <IconButton aria-label='Previous years' size='sm' variant='secondary' appearance='transparent' onClick={() => scrollerRef.current?.prev()}>
          <Icon><ChevronLeft /></Icon>
        </IconButton>
        <Text size={2} aria-live='polite'>{getPageStart(page)}–{getPageStart(page) + perPage - 1}</Text>
        <IconButton aria-label='Next years' size='sm' variant='secondary' appearance='transparent' onClick={() => scrollerRef.current?.next()}>
          <Icon><ChevronRight /></Icon>
        </IconButton>
      </div>
      <DataGridPickerScroller apiRef={scrollerRef} defaultIndex={0} onScrollSnapChanging={(_, index) => setPage(index)}>
        <DataGridPickerScrollerContent style={{ width: '100%' }}>
          <YearPage />
        </DataGridPickerScrollerContent>
      </DataGridPickerScroller>
    </DataGridPicker>
  );
}
```

### Error

#### Error: at least three training days

Multiple selection with a minimum. Submitting with fewer than three days turns the linked helper text into an error; reaching three clears it. The chosen days are submitted as repeated `day` fields.

```tsx
import { FormEvent, useState } from 'react';
import {
  Button,
  DataGridPicker,
  DataGridPickerItem,
  DataGridPickerRow,
  DataGridPickerRowGroup,
  HelperText,
  Text,
} from '@vega-ui/react';

const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const minimum = 3;

export default function AtLeastThreeDays() {
  const [selected, setSelected] = useState<string[]>(['Mon']);
  const [invalid, setInvalid] = useState(false);
  const [saved, setSaved] = useState('');

  const onSelectCell = (value: string[]) => {
    setSelected(value);
    if (value.length >= minimum) setInvalid(false);
  };

  const onSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();

    if (selected.length < minimum) {
      setInvalid(true);
      return;
    }

    setSaved(new FormData(event.currentTarget).getAll('day').join(', '));
  };

  return (
    <form onSubmit={onSubmit} style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%', maxWidth: 320 }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Text id='training-days-label' fontWeight={500}>Training days</Text>
        <DataGridPicker
          aria-labelledby='training-days-label'
          aria-describedby='training-days-message'
          aria-multiselectable='true'
          selection='multiple'
          size='sm'
          selected={selected}
          onSelectCell={onSelectCell}
          defaultActive='Mon'
          style={{ width: '100%' }}
        >
          <DataGridPickerRowGroup>
            <DataGridPickerRow row={0}>
              {days.map((day, col) => (
                <DataGridPickerItem key={day} col={col} value={day}>{day}</DataGridPickerItem>
              ))}
            </DataGridPickerRow>
          </DataGridPickerRowGroup>
        </DataGridPicker>
        {selected.map((day) => <input key={day} type='hidden' name='day' value={day} />)}
        <HelperText id='training-days-message' error={invalid}>
          {invalid ? `Pick at least ${minimum} days, ${minimum - selected.length} more to go.` : `Pick at least ${minimum} days a week.`}
        </HelperText>
      </div>
      <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 'var(--spacing-4)' }}>
        <Button type='submit'>Save plan</Button>
      </div>
      {saved && <Text size={2} style={{ color: 'var(--label-secondary)' }}>Saved: {saved}</Text>}
    </form>
  );
}
```

### Disabled

#### Disabled: sold-out sizes

`disabled` takes the sold-out values. The picker excludes disabled items from keyboard movement by default, and their labels say "sold out" for screen readers.

```tsx
import {
  DataGridPicker,
  DataGridPickerItem,
  DataGridPickerRow,
  DataGridPickerRowGroup,
  HelperText,
} from '@vega-ui/react';

const sizes = [[36, 37, 38, 39, 40, 41], [42, 43, 44, 45, 46, 47]];
const soldOut = [37, 41, 42, 46];

export default function BlockedCellsInsideADensePicker() {
  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%', maxWidth: 300 }}>
      <DataGridPicker
        aria-label='Shoe size, EU'
        aria-describedby='shoe-size-hint'
        size='sm'
        disabled={soldOut}
        defaultActive={36}
        style={{ width: '100%' }}
      >
        <DataGridPickerRowGroup>
          {sizes.map((row, rowIndex) => (
            <DataGridPickerRow key={rowIndex} row={rowIndex}>
              {row.map((size, col) => (
                <DataGridPickerItem key={size} col={col} value={size} aria-label={soldOut.includes(size) ? `${size}, sold out` : String(size)}>
                  {size}
                </DataGridPickerItem>
              ))}
            </DataGridPickerRow>
          ))}
        </DataGridPickerRowGroup>
      </DataGridPicker>
      <HelperText id='shoe-size-hint'>Sold-out sizes are skipped by the arrow keys.</HelperText>
    </div>
  );
}
```

### Edge

#### Edge: pick six of 49

A selection limit built from a `disabled` predicate: once six numbers are picked, every other number is disabled. `excludeDisabled={false}` keeps them reachable with the arrow keys, otherwise the keyboard could only move between the six picked numbers.

```tsx
import { useState } from 'react';
import {
  Button,
  DataGridPicker,
  DataGridPickerItem,
  DataGridPickerRow,
  DataGridPickerRowGroup,
  Text,
} from '@vega-ui/react';

const limit = 6;
const rows = Array.from({ length: 7 }, (_, row) => row);
const cols = Array.from({ length: 7 }, (_, col) => col);

const describePicked = (picked: number[]) => {
  if (picked.length === 0) return `Pick ${limit} numbers.`;

  const sorted = [...picked].sort((a, b) => a - b);

  return `${picked.length} of ${limit} picked: ${sorted.join(', ')}`;
};

export default function LotteryNumbers() {
  const [picked, setPicked] = useState<number[]>([]);

  const isLocked = (value: number) => picked.length >= limit && !picked.includes(value);

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%', maxWidth: 252 }}>
      <DataGridPicker
        aria-label={`Lottery numbers, pick ${limit}`}
        aria-multiselectable='true'
        selection='multiple'
        size='xs'
        selected={picked}
        onSelectCell={setPicked}
        disabled={isLocked}
        excludeDisabled={false}
        defaultActive={1}
        style={{ width: '100%' }}
      >
        <DataGridPickerRowGroup>
          {rows.map((row) => (
            <DataGridPickerRow key={row} row={row}>
              {cols.map((col) => {
                const value = row * cols.length + col + 1;

                return <DataGridPickerItem key={value} col={col} value={value}>{value}</DataGridPickerItem>;
              })}
            </DataGridPickerRow>
          ))}
        </DataGridPickerRowGroup>
      </DataGridPicker>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--spacing-4)' }}>
        <Text size={2} style={{ color: 'var(--label-secondary)', fontVariantNumeric: 'tabular-nums' }}>{describePicked(picked)}</Text>
        <Button size='sm' variant='secondary' appearance='ghost' disabled={picked.length === 0} onClick={() => setPicked([])}>Clear</Button>
      </div>
    </div>
  );
}
```

## Anatomy

### Overview

`DataGridPicker` is a compound picker subsystem. It wraps `DataGridSelectable` and adds a picker-specific visual context for size and variant.

### Required Parts

- `DataGridPicker`: root picker layer
- `DataGridPickerRowGroup`
- `DataGridPickerRow`
- `DataGridPickerItem`

### Optional Parts

- `DataGridPickerScroller`
- `DataGridPickerScrollerContent`

### Composition Order

1. `DataGridPicker`
2. optional `DataGridPickerScroller`
3. `DataGridPickerScrollerContent`
4. `DataGridPickerRowGroup`
5. `DataGridPickerRow`
6. `DataGridPickerItem`

### Valid Composition Patterns

- static picker grids for months, quarters, or emoji sets
- paged scroller pickers with scoped row groups
- single, multiple, or range selection depending on the consumer subsystem

### Invalid Composition Patterns

- using plain layout nodes instead of picker items for selectable content
- omitting scope management in paged compositions that depend on page identity
- treating `DataGridPicker` like a generic data table
