# DataTable

DataTable is a table component that displays structured data and provides various interactions such as sorting, selection, editing, and pagination.
The examples below can be tested in the story canvas, and the displayed code can be reused as-is.

## Basic Usage

```tsx
<DataTable
  onPaginationModelChange={fn()}
  onSelectionModelChange={fn()}
  onRowClick={fn()}
  columns={[{
  field: 'dessert',
  headerName: 'Dessert (100g serving)',
  width: '40%'
}, {
  field: 'calories',
  headerName: 'Calories',
  type: 'number'
}, {
  field: 'fat',
  headerName: 'Fat (g)',
  type: 'number'
}, {
  field: 'carbs',
  headerName: 'Carbs (g)',
  type: 'number'
}, {
  field: 'protein',
  headerName: 'Protein (g)',
  type: 'number'
}]}
  rows={rows}
/>
```

| Field    | Description | Default |
| -------- | ----------- | ------- |
| editMode | —           | false   |

### Required Data Structure

- `rows` is an array, and by default, each row must include an `id` field.
- If there is no `id`, you can specify a row identifier using `getId`.

```tsx
<DataTable
  rows={rows}
  columns={columns}
/>

<DataTable
  rows={rows}
  columns={columns}
  getId={(row) => row.uuid}
/>
```

## Column Definition (ColumnDef)

### Basic Options

```tsx
const columns = [
  {
    field: 'name',
    headerName: 'Name',
    width: '30%',
    minWidth: '120px',
    maxWidth: '320px',
    sortable: true,
    description: 'Description displayed as a tooltip',
    headerClassName: 'table-head',
    cellClassName: ({ row }) => (row.isHot ? 'cell-hot' : undefined),
  },
];
```

### Column Types

DataTable supports various column types. Each type provides an appropriate input UI in edit mode.

#### text (default)

The default text type. If `type` is not specified, it defaults to text.

```tsx
{
  field: 'name',
  headerName: 'Name',
  type: 'text', // can be omitted
}
```

#### number

A type for numeric input.

```tsx
{
  field: 'quantity',
  headerName: 'Quantity',
  type: 'number',
}
```

#### currency

A type for currency input. Uses the `CurrencyInput` component.

```tsx
<DataTable rows={tableRows} columns={columns} editMode noWrap />
```

```tsx
{
  field: 'price',
  headerName: 'Price',
  type: 'currency',
  componentProps: {
    currencyDisplay: 'narrowSymbol',
    currency: 'USD',
  },
}
```

#### date

A type for date selection. Uses the `DatePicker` component.

```tsx
<DataTable rows={tableRows} columns={columns} editMode noWrap />
```

```tsx
{
  field: 'dueDate',
  headerName: 'Due Date',
  type: 'date',
  componentProps: {
    format: 'yyyy-MM-dd',
  },
}
```

#### select

A type for dropdown selection. Uses the `Select` component.

```tsx
<DataTable rows={tableRows} columns={columns} editMode noWrap />
```

```tsx
{
  field: 'status',
  headerName: 'Status',
  type: 'select',
  componentProps: {
    options: ['Pending', 'In Progress', 'Completed', 'Cancelled'],
  },
}
```

#### autocomplete

A type for autocomplete selection. Uses the `Autocomplete` component.

```tsx
<DataTable rows={tableRows} columns={columns} editMode noWrap />
```

```tsx
{
  field: 'assignee',
  headerName: 'Assignee',
  type: 'autocomplete',
  componentProps: {
    options: ['Alice', 'Bob', 'Charlie', 'Diana'],
    placeholder: 'Select assignee...',
  },
}
```

#### longText

A type for long text input. Uses the `Textarea` component.

```tsx
<DataTable rows={tableRows} columns={columns} editMode noWrap />
```

```tsx
{
  field: 'description',
  headerName: 'Description',
  type: 'longText',
  componentProps: {
    minRows: 2,
    maxRows: 4,
  },
}
```

#### link

A link type. Renders as a clickable link.

```tsx
<DataTable rows={tableRows} columns={columns} noWrap />
```

```tsx
{
  field: 'website',
  headerName: 'Website',
  type: 'link',
  componentProps: ({ row }) => ({
    href: row.website,
    target: '_blank',
  }),
}
```

#### actions

A type for row-level action buttons.

```tsx
<DataTable rows={rows4} columns={columns} pinnedColumns={{
  right: ['actions']
}} />
```

```tsx
{
  field: 'actions',
  type: 'actions',
  getActions: ({ row }) => [
    <Button key="view" size="sm">View</Button>,
    <Button key="delete" size="sm" color="danger">Delete</Button>,
  ],
}
```

### Using componentProps

`componentProps` can be specified as an object or a function. When specified as a function, you can dynamically set props based on row data.

```tsx
// Static props
componentProps: {
  options: ['A', 'B', 'C'],
}

// Dynamic props (function)
componentProps: ({ row, id }) => ({
  options: row.availableOptions,
  disabled: row.isLocked,
})
```

## Row Interactions

### Row Hover

```tsx
<DataTable {...args} />
```

### Checkbox Selection

```tsx
<DataTable {...args} />
```

#### Controlling Selection Model

To control checkbox selection, use `selectionModel` and `onSelectionModelChange` together.

```tsx
const [selectionModel, setSelectionModel] = useState<string[]>([]);

<DataTable
  rows={rows}
  columns={columns}
  checkboxSelection
  selectionModel={selectionModel}
  onSelectionModelChange={setSelectionModel}
/>;
```

#### Notes on Selection Control

- `isRowSelectable` can control whether each row is selectable.
- `disableSelectionOnClick` prevents selection when clicking on a row.
- `isTotalSelected` is used to represent the total selection state in server pagination scenarios.

```tsx
<DataTable
  rows={rows}
  columns={columns}
  checkboxSelection
  isRowSelectable={({ row }) => row.status !== 'Closed'}
  disableSelectionOnClick
  isTotalSelected
/>
```

### Applying Selection Conditions

```tsx
<Stack height="500px" gap={1}>
  <Typography level="title-sm" textColor="text.secondary">
    status가 Closed인 행은 선택할 수 없습니다.
  </Typography>
  <DataTable checkboxSelection stripe="even" hoverRow stickyHeader rows={rows3} columns={columns} selectionModel={selectionModel} onSelectionModelChange={newSelection => setSelectionModel(newSelection)} isRowSelectable={({
    row
  }) => row.status !== 'Closed'} />
</Stack>
```

### Disabling Click Selection

```tsx
<Stack height="500px">
  <Typography level="title-sm" textColor="text.secondary">
    Disable selection on click.
  </Typography>
  <DataTable checkboxSelection stripe="even" hoverRow selectionModel={selectedId} stickyHeader onSelectionModelChange={newSelection => setSelectedId(newSelection)} disableSelectionOnClick rows={rows4} columns={columns} />
</Stack>
```

### Total Selection (isTotalSelected)

Used to manage the total selection state including data beyond the current page in server pagination environments.

```tsx
<Stack gap={2}>
  <Box sx={{
    p: 2,
    bgcolor: 'primary.50',
    borderRadius: '8px'
  }}>
  <Typography level="body-sm">
    {isTotalSelected ? `All ${allRows.length} items are selected (across all pages)` : `${selectionModel.length} items selected on current page`}
  </Typography>
</Box>
<DataTable rows={pagedRows} columns={columns} checkboxSelection selectionModel={selectionModel} onSelectionModelChange={(newSelection, totalSelected) => {
  setSelectionModel(newSelection);
  if (totalSelected !== undefined) {
    setIsTotalSelected(totalSelected);
  }
}} isTotalSelected={isTotalSelected} pagination paginationMode="server" rowCount={allRows.length} paginationModel={paginationModel} onPaginationModelChange={setPaginationModel} noWrap />
</Stack>
```

```tsx
const [isTotalSelected, setIsTotalSelected] = useState(false);
const [selectionModel, setSelectionModel] = useState<string[]>([]);

<DataTable
  rows={rows}
  columns={columns}
  checkboxSelection
  pagination
  paginationMode="server"
  isTotalSelected={isTotalSelected}
  selectionModel={selectionModel}
  onSelectionModelChange={(newModel, totalSelected) => {
    setSelectionModel(newModel);
    if (totalSelected !== undefined) {
      setIsTotalSelected(totalSelected);
    }
  }}
/>;
```

## Keyboard Accessibility

DataTable supports keyboard interactions for improved accessibility.

### Shift+Click Range Selection

Hold the Shift key while clicking checkboxes to select or deselect a range of rows at once.
The selection state is applied based on the last clicked checkbox's resulting state.

```tsx
<Stack spacing={2}>
  <Typography level="body-sm">
    Hold Shift and click checkboxes to select a range of rows. The selection state is applied based on the first
    clicked checkbox&apos;s resulting state.
  </Typography>
  <DataTable {...args} selectionModel={selectionModel} onSelectionModelChange={setSelectionModel} />
  <Typography level="body-xs">Selected IDs: {selectionModel.join(', ') || 'None'}</Typography>
</Stack>
```

**How it works:**

1. Click a checkbox to select/deselect a row (this becomes the "anchor")
2. Hold Shift and click another checkbox
3. All rows between the anchor and the clicked row will be set to the same state

**Edge cases handled:**

- Works seamlessly with virtual scrolling (rows don't need to be visible)
- Selection anchor resets when data changes or page changes
- Respects `isRowSelectable` - non-selectable rows are skipped in range

### Keyboard Navigation

When a row has focus, use keyboard shortcuts to navigate and interact with rows.

```tsx
<Stack spacing={2}>
  <Typography level="body-sm">
    Click a row to focus, then use keyboard to navigate. Arrow Up/Down to move, Space to toggle selection,
    Home/End to jump to first/last row, PageUp/PageDown to move by 10 rows. Hold Shift with any navigation key to
    extend selection.
  </Typography>
  <DataTable {...args} selectionModel={selectionModel} onSelectionModelChange={setSelectionModel} />
  <Typography level="body-xs">Selected IDs: {selectionModel.join(', ') || 'None'}</Typography>
</Stack>
```

**Supported keys:**

| Key                    | Action                                      |
| ---------------------- | ------------------------------------------- |
| `↑` Arrow Up           | Move focus to previous row                  |
| `↓` Arrow Down         | Move focus to next row                      |
| `Space`                | Toggle checkbox selection                   |
| `Home`                 | Move focus to first row                     |
| `End`                  | Move focus to last row                      |
| `Page Up`              | Move focus up by 10 rows                    |
| `Page Down`            | Move focus down by 10 rows                  |
| `Shift + ↑/↓`          | Move focus and extend selection             |
| `Shift + Home/End`     | Jump to first/last row and extend selection |
| `Shift + Page Up/Down` | Move by 10 rows and extend selection        |

**Notes:**

- Click any row first to establish focus
- Tab key skips checkboxes and moves directly between rows
- Works with virtual scrolling - target row is automatically scrolled into view
- Follows WAI-ARIA Grid pattern for accessibility

## Styles and Layout

### Back Office Style

```tsx
<DataTable rows={args.rows} columns={args.columns} checkboxSelection={args.checkboxSelection} hoverRow={args.hoverRow} noWrap={args.noWrap} stripe={args.stripe} stickyHeader={args.stickyHeader} slots={args.slots} slotProps={args.slotProps} selectionModel={selectionModel} onSelectionModelChange={setSelectionModel} pagination={args.pagination} />
```

#### Commonly Used Table Options

- `stickyHeader`: Fixes the header
- `stripe`: Row striping (`"even" | "odd"`)
- `noWrap`: Prevents cell text wrapping
- `slots`, `slotProps`: Customize toolbar/footer/loading overlay/background

```tsx
<DataTable
  rows={rows}
  columns={columns}
  stickyHeader
  stripe="even"
  noWrap
  slots={{
    toolbar: CustomToolbar,
    footer: CustomFooter,
    loadingOverlay: CustomLoading,
  }}
  slotProps={{
    background: { style: { height: '300px' } },
  }}
/>
```

### Loading State

```tsx
<DataTable {...args} />
```

### Virtual Scrolling for Large Data

```tsx
<Stack height="500px">
  <Typography level="title-sm" textColor="text.secondary">
    Virtualized DataTable
  </Typography>
  <DataTable checkboxSelection stripe="even" hoverRow selectionModel={selectedId} stickyHeader onSelectionModelChange={newSelection => setSelectedId(newSelection)} rows={[...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4].map((row, i) => ({
    ...row,
    id: i
  }))} columns={columns} sortModel={[{
    field: 'number',
    sort: 'asc'
  }]} />
</Stack>
```

## Slots & SlotProps Customization

DataTable allows customization of various parts through `slots` and `slotProps`.

### Available Slots

| Slot             | Description                  |
| ---------------- | ---------------------------- |
| `checkbox`       | Customize checkbox component |
| `toolbar`        | Table top toolbar            |
| `footer`         | Table bottom footer          |
| `loadingOverlay` | Loading overlay              |

### Custom Toolbar

```tsx
<DataTable rows={filteredRows} columns={columns} slots={{
  toolbar: CustomToolbar
}} noWrap stickyHeader stripe="even" />
```

```tsx
const CustomToolbar = () => (
  <Box sx={{ p: 2, display: 'flex', gap: 1, borderBottom: '1px solid', borderColor: 'neutral.200' }}>
    <Button size="sm">Export</Button>
    <Button size="sm">Filter</Button>
  </Box>
);

<DataTable rows={rows} columns={columns} slots={{ toolbar: CustomToolbar }} />;
```

### Custom Footer

```tsx
<DataTable rows={tableRows} columns={columns} slots={{
  footer: CustomFooter
}} noWrap />
```

```tsx
const CustomFooter = () => (
  <Box sx={{ p: 2, textAlign: 'center', borderTop: '1px solid', borderColor: 'neutral.200' }}>
    <Typography level="body-sm">Total {rows.length} items</Typography>
  </Box>
);

<DataTable rows={rows} columns={columns} slots={{ footer: CustomFooter }} />;
```

### Custom Loading Overlay

```tsx
<Stack gap={2}>
  <Button onClick={() => setLoading(!loading)}>{loading ? 'Stop Loading' : 'Start Loading'}</Button>
  <DataTable rows={tableRows} columns={columns} loading={loading} slots={{
    loadingOverlay: CustomLoadingOverlay
  }} noWrap slotProps={{
    background: {
      style: {
        height: '300px'
      }
    }
  }} />
</Stack>
```

```tsx
const CustomLoadingOverlay = () => (
  <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 }}>
    <CircularProgress />
    <Typography>Loading data...</Typography>
  </Box>
);

<DataTable rows={rows} columns={columns} loading slots={{ loadingOverlay: CustomLoadingOverlay }} />;
```

### Custom Checkbox

```tsx
<DataTable rows={tableRows} columns={columns} checkboxSelection selectionModel={selectionModel} onSelectionModelChange={newSelection => setSelectionModel(newSelection)} slots={{
  checkbox: CustomCheckbox
}} noWrap />
```

```tsx
const CustomCheckbox = (props) => (
  <Box
    onClick={props.onChange}
    sx={{
      width: 20,
      height: 20,
      borderRadius: '4px',
      border: '2px solid',
      borderColor: props.checked ? 'primary.500' : 'neutral.400',
      backgroundColor: props.checked ? 'primary.500' : 'transparent',
      cursor: 'pointer',
    }}
  >
    {props.checked && <span>✓</span>}
  </Box>
);

<DataTable rows={rows} columns={columns} checkboxSelection slots={{ checkbox: CustomCheckbox }} />;
```

### Background Styling (slotProps.background)

You can style the table background area using `slotProps.background`. Height specification is required when used with `stickyHeader`.

```tsx
<DataTable
  rows={rows}
  columns={columns}
  stickyHeader
  slotProps={{
    background: {
      style: { height: '400px' },
    },
  }}
/>
```

## Column Configuration

### Displaying id Column

```tsx
<DataTable {...args} />
```

### Customizing Column Rendering

```tsx
<DataTable rows={useMemo(() => [...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3].map((row, i) => ({
  ...row,
  id: i + 1
})), [])} columns={columns} checkboxSelection stickyHeader stripe="even" noWrap slotProps={{
  background: {
    style: {
      height: '600px'
    }
  }
}} />
```

### Complex UI in Cells

```tsx
<DataTable rows={rows4} columns={columns} />
```

### Description (tooltip)

```tsx
<DataTable rows={rows4} columns={columns} />
```

### Customizing Cell/Header Classes

```tsx
<DataTable
  onPaginationModelChange={fn()}
  onSelectionModelChange={fn()}
  onRowClick={fn()}
  columns={[{
  field: 'id',
  headerName: 'ID',
  width: '150px',
  cellClassName: ({
    row
  }) => row.number > 5 ? 'red' : 'blue'
}, {
  field: 'number',
  headerName: 'Number',
  type: 'number',
  width: '150px',
  cellClassName: ({
    value
  }) => value > 5 ? 'red' : 'blue'
}, {
  field: 'string',
  headerName: 'String',
  width: '600px',
  cellClassName: ({
    row
  }) => row.number > 5 ? 'red' : 'blue'
}]}
  rows={rows4}
/>
```

```tsx
<DataTable
  onPaginationModelChange={fn()}
  onSelectionModelChange={fn()}
  onRowClick={fn()}
  columns={[{
  field: 'id',
  headerName: 'ID',
  width: '150px',
  headerClassName: 'red'
}, {
  field: 'number',
  headerName: 'Number',
  type: 'number',
  width: '150px',
  headerClassName: 'red'
}, {
  field: 'string',
  headerName: 'String',
  width: '600px',
  headerClassName: 'red'
}, {
  field: 'date',
  headerName: 'Date Sort',
  width: '150px',
  renderCell: ({
    value
  }) => value.toLocaleDateString(),
  headerClassName: 'blue'
}, {
  field: 'object',
  headerName: 'Object Sort',
  width: '150px',
  renderCell: ({
    value
  }) => value.age,
  type: 'number',
  sortComparator: ({
    rowA,
    rowB
  }) => rowA.object.age - rowB.object.age,
  headerClassName: 'blue'
}]}
  rows={rows4}
  checkboxSelection
  hoverRow
  noWrap
  stripe="even"
  stickyHeader
  columnGroupingModel={[{
  groupId: 'group1',
  headerName: 'Group 1',
  headerClassName: 'red',
  children: [{
    field: 'id'
  }, {
    field: 'number'
  }, {
    field: 'string'
  }]
}, {
  groupId: 'group2',
  headerName: '',
  children: [{
    field: 'date'
  }, {
    field: 'object'
  }]
}]}
/>
```

### Column Groups

```tsx
<DataTable
  onPaginationModelChange={fn()}
  onSelectionModelChange={fn()}
  onRowClick={fn()}
  columns={[{
  field: 'id',
  headerName: 'ID',
  width: '150px'
}, {
  field: 'number',
  headerName: 'Number Sort',
  type: 'number',
  width: '900px',
  cellClassName: () => 'warn'
}, {
  field: 'string',
  headerName: 'String Sort',
  width: '150px'
}, {
  field: 'date',
  headerName: 'Date Sort',
  width: '150px',
  renderCell: ({
    value
  }) => value.toLocaleDateString(),
  cellClassName: () => 'primary'
}, {
  field: 'object',
  headerName: 'Object Sort',
  width: '150px',
  renderCell: ({
    value
  }) => value.age,
  type: 'number',
  sortComparator: ({
    rowA,
    rowB
  }) => rowA.object.age - rowB.object.age,
  cellClassName: () => 'primary'
}]}
  rows={[...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4].map((row, i) => ({
  ...row,
  id: i,
  date: new Date(row.date),
  object: {
    age: row.object.age + i
  }
}))}
  checkboxSelection
  hoverRow
  noWrap
  stripe="even"
  stickyHeader
  columnGroupingModel={[{
  groupId: 'group1',
  headerName: 'Group 1',
  children: [{
    field: 'id'
  }, {
    field: 'number'
  }, {
    field: 'string'
  }]
}, {
  groupId: 'group2',
  headerName: '',
  children: [{
    field: 'date'
  }, {
    field: 'object'
  }]
}]}
  slotProps={{
  background: {
    style: {
      height: '300px'
    }
  }
}}
/>
```

```tsx
const columnGroupingModel = [
  {
    groupId: 'nutrition',
    headerName: 'Nutrition',
    children: [{ field: 'calories' }, { field: 'fat' }, { field: 'carbs' }],
  },
];

<DataTable rows={rows} columns={columns} columnGroupingModel={columnGroupingModel} />;
```

### Column Resizing

Setting `resizable: true` on a column allows you to adjust the column width by dragging.

```tsx
<Stack gap={1}>
  <Typography level="body-sm" textColor="text.secondary">
    Drag the column borders to resize columns
  </Typography>
  <DataTable rows={tableRows} columns={columns} noWrap />
</Stack>
```

```tsx
const columns = [
  {
    field: 'name',
    headerName: 'Name',
    width: '200px',
    resizable: true,
  },
  {
    field: 'description',
    headerName: 'Description',
    width: '300px',
    resizable: true,
  },
];
```

## Editing Features

### Inline Editing

```tsx
<div>
  <DataTable rows={rows3} columns={columns} checkboxSelection={false} noWrap editMode />
  <Button onClick={() => {
    console.log(editedRows);
    setIsCellEditable(true);
  }}>
  Submit
</Button>
</div>
```

#### Edit Types

- `text`, `number`, `currency`, `select`, `autocomplete`, `date`, `longText`, `link`

#### Notes on Editing

- `editMode` must be enabled for the editing UI to be active.
- Use `onCellEditStop` or `onCellEditStart` to reflect changed values.
- Specifying `renderEditCell` allows you to customize the editing UI.

```tsx
const columns = [
  {
    field: 'status',
    type: 'select',
    componentProps: { options: ['Opened', 'Preparing', 'Closed'] },
    onCellEditStop: ({ row }) => updateRow(row),
  },
  {
    field: 'note',
    type: 'longText',
    renderEditCell: ({ value }) => value.slice(0, 100),
  },
];

<DataTable rows={rows} columns={columns} editMode />;
```

### Conditional Editing

```tsx
<div>
  <Typography level="title-lg" textColor="text.secondary">
    expectedSales 100초과만 수정 가능
  </Typography>
  <DataTable rows={rows} columns={columns} checkboxSelection={false} noWrap editMode />
  <Button onClick={() => {
    console.log(rows);
  }}>
  Submit
</Button>
</div>
```

### Real-time Data Updates

```tsx
<DataTable rows={editRows} columns={columns} checkboxSelection={false} noWrap />
```

### Required Field Indicator

Setting `required: true` on a column displays a required indicator (\*) in the header.

```tsx
<Stack gap={1}>
  <Typography level="body-sm" textColor="text.secondary">
    Fields marked with * are required
  </Typography>
  <DataTable rows={tableRows} columns={columns} editMode noWrap />
</Stack>
```

```tsx
const columns = [
  {
    field: 'name',
    headerName: 'Name',
    required: true,
  },
  {
    field: 'email',
    headerName: 'Email',
    required: true,
  },
];
```

### Customizing renderEditCell

Using `renderEditCell` allows you to fully customize the editing UI.

```tsx
<DataTable rows={tableRows} columns={columns} editMode noWrap />
```

```tsx
{
  field: 'rating',
  headerName: 'Rating',
  renderEditCell: ({ value, row }) => (
    <Box sx={{ display: 'flex', gap: 0.5 }}>
      {[1, 2, 3, 4, 5].map((star) => (
        <IconButton
          key={star}
          size="sm"
          onClick={() => updateRating(row.id, star)}
        >
          {star <= value ? '★' : '☆'}
        </IconButton>
      ))}
    </Box>
  ),
}
```

### Handling Edit Events

You can use `onCellEditStart` and `onCellEditStop` to detect when editing starts and ends.

```tsx
<Stack gap={2}>
  <DataTable rows={tableRows} columns={columns} editMode noWrap />
  <Box sx={{
    p: 2,
    bgcolor: 'neutral.100',
    borderRadius: '8px',
    fontFamily: 'monospace',
    fontSize: '12px'
  }}>
  <Typography level="title-sm" sx={{
    mb: 1
  }}>
  Edit Log:
</Typography>
{editLog.length === 0 ? <Typography level="body-xs" textColor="text.secondary">
No edits yet. Click on a cell to edit.
</Typography> : editLog.map((log, i) => <Typography key={i} level="body-xs">
{log}
</Typography>)}
</Box>
</Stack>
```

```tsx
{
  field: 'value',
  headerName: 'Value',
  onCellEditStart: ({ row, originalRow, value }) => {
    console.log('Edit started:', { row, originalRow, value });
  },
  onCellEditStop: ({ row, originalRow, value }) => {
    console.log('Edit stopped:', { row, originalRow, value });
    // Save changes to server
    saveChanges(row);
  },
}
```

## Event Handlers

### onRowClick

Handles row click events. Useful for implementing Inspector patterns or detail view features.

```tsx
<Stack gap={2}>
  <DataTable rows={tableRows} columns={columns} hoverRow onRowClick={({
    row
  }) => setSelectedRow(row)} noWrap />
  {selectedRow && <Box sx={{
    p: 2,
    border: '1px solid',
    borderColor: 'neutral.300',
    borderRadius: '8px'
  }}>
  <Typography level="title-md">Selected Employee Details</Typography>
  <Typography level="body-sm">Name: {selectedRow.name}</Typography>
  <Typography level="body-sm">Email: {selectedRow.email}</Typography>
  <Typography level="body-sm">Department: {selectedRow.department}</Typography>
</Box>}
</Stack>
```

```tsx
const [selectedRow, setSelectedRow] = useState(null);

<DataTable
  rows={rows}
  columns={columns}
  hoverRow
  onRowClick={({ row, rowId }, event) => {
    console.log('Clicked row:', row, 'ID:', rowId);
    setSelectedRow(row);
  }}
/>;
```

## Sorting Features

### Basic Sorting

```tsx
<DataTable rows={rows4} columns={columns} />
```

### Custom Sort Order

```tsx
<DataTable rows={rows4} columns={columns} sortOrder={['desc', 'asc']} />
```

### Column-specific Sort Order

```tsx
<DataTable rows={rows4} columns={columns} />
```

### Disabling Sorting

```tsx
<DataTable rows={rows4} columns={columns} />
```

### Multiple Sorting

```tsx
<DataTable rows={rows4} columns={columns} />
```

### Custom Sort Logic

```tsx
<DataTable rows={rows4} columns={columns} />
```

### Controlling Sort State

#### Uncontrolled Initial Sort

```tsx
<DataTable
  onPaginationModelChange={fn()}
  onSelectionModelChange={fn()}
  onRowClick={fn()}
  columns={[{
  field: 'dessert',
  headerName: 'Dessert (100g serving)',
  width: '40%'
}, {
  field: 'calories',
  headerName: 'Calories',
  type: 'number'
}, {
  field: 'fat',
  headerName: 'Fat (g)',
  type: 'number'
}, {
  field: 'carbs',
  headerName: 'Carbs (g)',
  type: 'number'
}, {
  field: 'protein',
  headerName: 'Protein (g)',
  type: 'number'
}]}
  rows={rows}
  initialState={{
  sorting: {
    sortModel: [{
      field: 'calories',
      sort: 'asc'
    }]
  }
}}
/>
```

#### Controlled Sort

```tsx
<DataTable rows={rows4} columns={columns} sortModel={sortModel} onSortModelChange={handleSortModelChange} />
```

```tsx
const [sortModel, setSortModel] = useState([{ field: 'calories', sort: 'asc' }]);

<DataTable rows={rows} columns={columns} sortModel={sortModel} onSortModelChange={setSortModel} />;
```

## Pagination

### Basic Pagination

```tsx
<DataTable
  onPaginationModelChange={fn()}
  onSelectionModelChange={fn()}
  onRowClick={fn()}
  columns={[{
  field: 'dessert',
  headerName: 'Dessert (100g serving)',
  width: '40%'
}, {
  field: 'calories',
  headerName: 'Calories',
  type: 'number'
}, {
  field: 'fat',
  headerName: 'Fat (g)',
  type: 'number'
}, {
  field: 'carbs',
  headerName: 'Carbs (g)',
  type: 'number'
}, {
  field: 'protein',
  headerName: 'Protein (g)',
  type: 'number'
}]}
  rows={[...rows, ...rows, ...rows, ...rows, ...rows, ...rows, ...rows, ...rows, ...rows, ...rows, ...rows, ...rows, ...rows, ...rows, ...rows, ...rows, ...rows, ...rows, ...rows, ...rows].map((row, index) => ({
  ...row,
  id: index
}))}
  pagination
  noWrap
  slotProps={{
  background: {
    style: {
      height: '300px'
    }
  }
}}
/>
```

### Empty State

```tsx
<DataTable
  onPaginationModelChange={fn()}
  onSelectionModelChange={fn()}
  onRowClick={fn()}
  columns={[{
  field: 'dessert',
  headerName: 'Dessert (100g serving)',
  width: '40%'
}, {
  field: 'calories',
  headerName: 'Calories',
  type: 'number'
}, {
  field: 'fat',
  headerName: 'Fat (g)',
  type: 'number'
}, {
  field: 'carbs',
  headerName: 'Carbs (g)',
  type: 'number'
}, {
  field: 'protein',
  headerName: 'Protein (g)',
  type: 'number'
}]}
  rows={[]}
  pagination
  noWrap
  slotProps={{
  background: {
    style: {
      height: '300px'
    }
  }
}}
/>
```

### Sorting + Pagination

```tsx
<DataTable rows={rows} columns={columns} pagination slotProps={{
  background: {
    style: {
      height: '300px'
    }
  }
}} getId={(row: RowForSort) => row.id} />
```

### Server Pagination

```tsx
<DataTable rows={pagedRows} columns={columns as any} pagination paginationMode="server" rowCount={allRows.length} paginationModel={paginationModel} onPaginationModelChange={setPaginationModel} loading={loading} />
```

You can specify the total row count using `paginationMode="server"` and `rowCount`.

```tsx
<DataTable
  rows={rows}
  columns={columns}
  pagination
  paginationMode="server"
  rowCount={totalRowCount}
  paginationModel={{ page: 0, pageSize: 20 }}
  onPaginationModelChange={setPaginationModel}
/>
```

### Dynamic rowCount

```tsx
<Stack gap={4} height="500px">
  <Stack direction="row" gap={2}>
    <Button onClick={handleIncrease}>Increase</Button>
    <Button onClick={handleDecrease}>Decrease</Button>
  </Stack>
  <DataTable rows={rows} columns={columns} pagination getId={(row: RowForSort) => row.id} />
</Stack>
```

## Advanced Layout

### Pinned Columns

```tsx
<DataTable rows={args.rows} columns={args.columns} pinnedColumns={args.pinnedColumns} stickyHeader={args.stickyHeader} stripe={args.stripe} noWrap={args.noWrap} hoverRow={args.hoverRow} selectionModel={selectionModel} onSelectionModelChange={setSelectionModel} />
```

```tsx
<DataTable rows={rows} columns={columns} pinnedColumns={{ left: ['id', 'name'], right: ['actions'] }} />
```

## API Usage

### DataTableApi Interface

DataTable supports programmatic control through `apiRef`.

```tsx
interface DataTableApi {
  getRowIndexRelativeToVisibleRows(rowId: string): number;
  setCellFocus(rowId: string): void;
}
```

### Scrolling/Focusing to a Specific Row

```tsx
<Box sx={{
  height: '400px'
}}>
<DataTable ref={ref} rows={args.rows} columns={args.columns} checkboxSelection={args.checkboxSelection} disableSelectionOnClick={args.disableSelectionOnClick} selectionModel={args.selectionModel} isTotalSelected={args.isTotalSelected} pagination={args.pagination} paginationMode={args.paginationMode} paginationModel={args.paginationModel} noWrap={args.noWrap} hoverRow={args.hoverRow} stripe={args.stripe} stickyHeader={args.stickyHeader} initialState={args.initialState} />
</Box>
```

### Inspector Pattern

```tsx
<Stack sx={{
  height: '100vh'
}}>
<Typography level="title-sm" textColor="text.secondary">
  Virtualized & Inspector DataTable
</Typography>
<Button onClick={() => apiRef.current?.setCellFocus('0')}>Row ID 0 Focus</Button>
<Button onClick={() => apiRef.current?.setCellFocus('100')}>Row ID 100 Focus</Button>
<div style={{
  flex: 1,
  maxHeight: '100%',
  overflow: 'auto'
}}>
<DataTable ref={apiRef} rows={args.rows} columns={args.columns} stripe={args.stripe} hoverRow={args.hoverRow} stickyHeader={args.stickyHeader} disableSelectionOnClick={args.disableSelectionOnClick} checkboxSelection={args.checkboxSelection} noWrap={args.noWrap} selectionModel={selectedRowId === -1 ? [] : [selectedRowId]} onRowClick={({
  rowId
}) => {
  if (selectedRowId === rowId) {
    setSelectedRowId(-1);
  } else {
    console.log(rowId);
    setSelectedRowId(rowId);
    setTimeout(() => {
      apiRef.current?.setCellFocus(String(rowId));
    }, 0);
  }
}} />
</div>
{selectedRowId !== -1 && <Box sx={{
  backgroundColor: 'aqua',
  border: 'black 1px solid',
  height: '300px'
}}>
This is inspector: {selectedRowId}
</Box>}
</Stack>
```

## Constraints and Considerations

### Required Conditions

- Using `useMemo` to maintain references when `rows` changes can reduce unnecessary re-renders.
- When using custom `getId`, values must be unique; duplicates can cause selection/sorting/editing to malfunction.
- Without height on `slotProps.background`, `stickyHeader` may not display properly.

### Feature Combination Constraints

- `pinnedColumns` and `columnGroupingModel` cannot be used simultaneously.
- `editMode` must be `true` for editing-related UI to be active.
- When `paginationMode="server"`, `rowCount` must be specified.

### Performance Optimization

- Use virtual scrolling for large data (1000+ rows).
- Memoize the `columns` array with `useMemo`.
- Be careful with `renderCell` and `componentProps` functions as they can cause unnecessary re-renders.

```tsx
// Good
const columns = useMemo(() => [...], [dependencies]);

// Bad - creates a new array on every render
const columns = [...];
```
