npm package: @mayur.sarvadhi/virtual-grid
Description: A high-performance, feature-rich virtualized grid component for React with sorting, filtering, cell selection, row selection, column resize, and more.
Latest version: 1.0.1

# VirtualGrid

A high-performance, feature-rich virtualized grid component for React. Built to handle millions of rows with smooth scrolling, sorting, filtering, cell selection, row selection, column resizing, and more.

## Features

✨ **Core Features**

- 🚀 **Virtual Scrolling** - Only renders visible rows for optimal performance
- 📊 **Large Dataset Support** - Handles 1M+ rows smoothly
- 🔄 **Sorting** - Client-side sorting with custom sorters
- 🔍 **Filtering** - Column-based filtering with searchable dropdowns
- 📝 **Cell Selection** - Excel-like cell selection with keyboard navigation
- ☑️ **Row Selection** - Single and multi-row selection with checkboxes
- 📏 **Column Resize** - Drag to resize columns
- 🔀 **Column Reorder** - Drag and drop columns to reorder
- ✏️ **Inline Editing** - Edit cells directly in the grid
- 🎨 **Theming** - Fully customizable themes
- 📱 **Responsive** - Mobile-friendly design
- ⌨️ **Keyboard Navigation** - Full keyboard support for cell selection
- 📋 **Copy to Clipboard** - Copy selected cells (Ctrl+C / Cmd+C)

## Installation

```bash
npm install @mayur.sarvadhi/virtual-grid
# or
yarn add @mayur.sarvadhi/virtual-grid
# or
pnpm add @mayur.sarvadhi/virtual-grid
```

## Peer Dependencies

This package requires React 18 or 19:

```bash
npm install react react-dom
```

## Quick Start

```tsx
import React from "react";
import VirtualGrid, { Column } from "@mayur.sarvadhi/virtual-grid";
import "@mayur.sarvadhi/virtual-grid/dist/VirtualGrid.css";

const App = () => {
  const data = [
    { id: 1, name: "John Doe", email: "john@example.com", age: 30 },
    { id: 2, name: "Jane Smith", email: "jane@example.com", age: 25 },
    // ... more data
  ];

  const columns: Column[] = [
    { key: "id", dataIndex: "id", title: "ID", width: 80 },
    { key: "name", dataIndex: "name", title: "Name", width: 150 },
    { key: "email", dataIndex: "email", title: "Email", width: 220 },
    { key: "age", dataIndex: "age", title: "Age", width: 80 },
  ];

  return (
    <div style={{ height: "600px" }}>
      <VirtualGrid dataSource={data} columns={columns} />
    </div>
  );
};
```

## API Reference

### VirtualGrid Props

| Prop                   | Type                                               | Default      | Description                                   |
| ---------------------- | -------------------------------------------------- | ------------ | --------------------------------------------- |
| `dataSource`           | `Record<string, unknown>[]`                        | **Required** | Array of data objects to display              |
| `originalDataSource`   | `Record<string, unknown>[]`                        | `undefined`  | Original unfiltered data (for filtering)      |
| `columns`              | `Column[]`                                         | **Required** | Column configuration array                    |
| `rowHeight`            | `number`                                           | `30`         | Height of each row in pixels                  |
| `headerHeight`         | `number`                                           | `30`         | Height of header row in pixels                |
| `overscan`             | `number`                                           | `3`          | Number of rows to render outside visible area |
| `style`                | `React.CSSProperties`                              | `undefined`  | Custom styles for the grid container          |
| `className`            | `string`                                           | `''`         | Additional CSS class name                     |
| `rowClassName`         | `(record, index) => string`                        | `undefined`  | Function to generate row class names          |
| `sortState`            | `SortState`                                        | `undefined`  | Controlled sort state                         |
| `onSort`               | `(columnKey, direction) => void`                   | `undefined`  | Callback when column is sorted                |
| `enableCellSelection`  | `boolean`                                          | `false`      | Enable Excel-like cell selection              |
| `onCellSelection`      | `(selectedCells) => void`                          | `undefined`  | Callback when cell selection changes          |
| `enableRowSelection`   | `boolean`                                          | `false`      | Enable row selection with checkboxes          |
| `rowKey`               | `string`                                           | `undefined`  | Unique key field in data (defaults to index)  |
| `selectedRowKeys`      | `Array<string \| number>`                          | `undefined`  | Controlled selected row keys                  |
| `onRowSelectionChange` | `(keys, rows) => void`                             | `undefined`  | Callback when row selection changes           |
| `enableColumnResize`   | `boolean`                                          | `false`      | Enable column resizing                        |
| `onColumnResize`       | `(columnKey, newWidth) => void`                    | `undefined`  | Callback when column is resized               |
| `columnWidths`         | `Record<string, number>`                           | `{}`         | Controlled column widths                      |
| `enableColumnReorder`  | `boolean`                                          | `false`      | Enable column reordering                      |
| `onColumnReorder`      | `(sourceIndex, targetIndex) => void`               | `undefined`  | Callback when column is reordered             |
| `filters`              | `Record<string, Set<string \| number \| boolean>>` | `undefined`  | Controlled filter state                       |
| `onFiltersChange`      | `(filters) => void`                                | `undefined`  | Callback when filters change                  |
| `onCellEdit`           | `(params) => void`                                 | `undefined`  | Callback when cell is edited                  |
| `onRowClick`           | `(record, index) => void`                          | `undefined`  | Callback when row is clicked                  |
| `theme`                | `Theme`                                            | `{}`         | Theme configuration object                    |

### Column Interface

```typescript
interface Column {
  key: string; // Unique column identifier
  dataIndex: string; // Field name in data object
  title: string; // Column header text
  width?: number; // Column width (default: 150)
  minWidth?: number; // Minimum column width
  align?: "left" | "center" | "right"; // Text alignment
  sortable?: boolean; // Enable sorting
  sorter?: (a, b) => number; // Custom sort function
  filterable?: boolean; // Enable filtering
  editable?: boolean; // Enable inline editing
  editor?: EditorConfig; // Editor configuration
  render?: (value, record, index) => React.ReactNode; // Custom cell renderer
}
```

### Editor Configuration

```typescript
// Text input
editor: { type: 'text' }

// Number input
editor: { type: 'number' }

// Date picker
editor: { type: 'date' }

// Checkbox
editor: { type: 'checkbox' }

// Select dropdown
editor: {
  type: 'select',
  options: [
    { label: 'Option 1', value: 'value1' },
    { label: 'Option 2', value: 'value2' }
  ]
}
```

### Theme Interface

```typescript
interface Theme {
  headerBackground?: string; // Header background color
  headerColor?: string; // Header text color
  rowBackground?: string; // Row background color
  rowAlternateBackground?: string; // Alternating row background
  rowHoverBackground?: string; // Row hover background
  borderColor?: string; // Border color
  fontSize?: string; // Font size
  fontFamily?: string; // Font family
  rowColor?: string; // Row text color
}
```

## Callbacks Reference

### `onSort(columnKey: string, direction: 'asc' | 'desc' | null) => void`

Called when a sortable column header is clicked.

**Parameters:**

- `columnKey`: The key of the column being sorted
- `direction`: Sort direction (`'asc'`, `'desc'`, or `null` to clear)

**Example:**

```tsx
const [sortState, setSortState] = useState<SortState>({
  columnKey: null,
  direction: null,
});

const handleSort = (columnKey: string, direction: "asc" | "desc" | null) => {
  setSortState({ columnKey, direction });
  // Perform sorting logic here
};

<VirtualGrid
  sortState={sortState}
  onSort={handleSort}
  // ... other props
/>;
```

### `onRowSelectionChange(selectedRowKeys: Array<string | number>, selectedRows: Record<string, unknown>[]) => void`

Called when row selection changes (checkbox clicked or programmatically).

**Parameters:**

- `selectedRowKeys`: Array of selected row keys
- `selectedRows`: Array of selected row data objects

**Example:**

```tsx
const [selectedKeys, setSelectedKeys] = useState<Array<string | number>>([]);

const handleSelectionChange = (
  keys: Array<string | number>,
  rows: Record<string, unknown>[]
) => {
  setSelectedKeys(keys);
  console.log("Selected:", rows);
};

<VirtualGrid
  enableRowSelection
  rowKey="id"
  selectedRowKeys={selectedKeys}
  onRowSelectionChange={handleSelectionChange}
  // ... other props
/>;
```

### `onCellSelection(selectedCells: CellSelection[]) => void`

Called when cell selection changes (click, drag, or keyboard navigation).

**Parameters:**

- `selectedCells`: Array of selected cells with structure:
  ```typescript
  {
    rowIndex: number;
    columnKey: string;
    value: unknown;
  }
  [];
  ```

**Example:**

```tsx
const handleCellSelection = (cells: CellSelection[]) => {
  console.log("Selected cells:", cells);
  // Copy to clipboard, export, etc.
};

<VirtualGrid
  enableCellSelection
  onCellSelection={handleCellSelection}
  // ... other props
/>;
```

**Keyboard Shortcuts:**

- `Arrow Keys`: Navigate between cells
- `Shift + Arrow Keys`: Select range
- `Ctrl/Cmd + C`: Copy selected cells to clipboard
- `Home/End`: Jump to first/last column
- `Page Up/Down`: Jump by 10 rows

### `onColumnResize(columnKey: string, newWidth: number) => void`

Called when a column is resized by dragging.

**Parameters:**

- `columnKey`: The key of the resized column
- `newWidth`: New width in pixels

**Example:**

```tsx
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});

const handleResize = (columnKey: string, newWidth: number) => {
  setColumnWidths((prev) => ({ ...prev, [columnKey]: newWidth }));
};

<VirtualGrid
  enableColumnResize
  columnWidths={columnWidths}
  onColumnResize={handleResize}
  // ... other props
/>;
```

### `onColumnReorder(sourceIndex: number, targetIndex: number) => void`

Called when a column is dragged to a new position.

**Parameters:**

- `sourceIndex`: Original column index
- `targetIndex`: New column index

**Example:**

```tsx
const [columns, setColumns] = useState<Column[]>(initialColumns);

const handleReorder = (sourceIndex: number, targetIndex: number) => {
  const newColumns = [...columns];
  const [removed] = newColumns.splice(sourceIndex, 1);
  newColumns.splice(targetIndex, 0, removed);
  setColumns(newColumns);
};

<VirtualGrid
  enableColumnReorder
  columns={columns}
  onColumnReorder={handleReorder}
  // ... other props
/>;
```

### `onFiltersChange(filters: Record<string, Set<string | number | boolean>>) => void`

Called when column filters are applied or cleared.

**Parameters:**

- `filters`: Object mapping column keys to sets of selected filter values

**Example:**

```tsx
const [filters, setFilters] = useState<
  Record<string, Set<string | number | boolean>>
>({});

const handleFiltersChange = (
  nextFilters: Record<string, Set<string | number | boolean>>
) => {
  setFilters(nextFilters);
  // Apply filters to dataSource
};

<VirtualGrid
  filters={filters}
  onFiltersChange={handleFiltersChange}
  // ... other props
/>;
```

### `onCellEdit(params: { rowIndex: number, rowKeyValue: string | number, columnKey: string, dataIndex: string, value: unknown }) => void`

Called when an editable cell value is changed.

**Parameters:**

- `rowIndex`: Index of the edited row
- `rowKeyValue`: Unique key value of the row
- `columnKey`: Key of the edited column
- `dataIndex`: Data field name
- `value`: New cell value

**Example:**

```tsx
const handleCellEdit = (params: {
  rowIndex: number;
  rowKeyValue: string | number;
  columnKey: string;
  dataIndex: string;
  value: unknown;
}) => {
  // Update your data source
  const updatedData = [...dataSource];
  updatedData[params.rowIndex][params.dataIndex] = params.value;
  setDataSource(updatedData);
};

<VirtualGrid
  onCellEdit={handleCellEdit}
  // ... other props
/>;
```

### `onRowClick(record: Record<string, unknown>, index: number) => void`

Called when a row is clicked.

**Parameters:**

- `record`: The row data object
- `index`: Row index

**Example:**

```tsx
const handleRowClick = (record: Record<string, unknown>, index: number) => {
  console.log("Clicked row:", record);
  // Navigate to detail page, show modal, etc.
};

<VirtualGrid
  onRowClick={handleRowClick}
  // ... other props
/>;
```

## Usage Examples

### Basic Grid

```tsx
import VirtualGrid, { Column } from "@mayur.sarvadhi/virtual-grid";
import "@mayur.sarvadhi/virtual-grid/dist/VirtualGrid.css";

const data = [
  { id: 1, name: "John", age: 30 },
  { id: 2, name: "Jane", age: 25 },
];

const columns: Column[] = [
  { key: "id", dataIndex: "id", title: "ID", width: 80 },
  { key: "name", dataIndex: "name", title: "Name", width: 150 },
  { key: "age", dataIndex: "age", title: "Age", width: 80 },
];

<VirtualGrid dataSource={data} columns={columns} />;
```

### Sorting

```tsx
const [sortState, setSortState] = useState<SortState>({
  columnKey: null,
  direction: null,
});

const [sortedData, setSortedData] = useState(data);

const handleSort = (columnKey: string, direction: "asc" | "desc" | null) => {
  setSortState({ columnKey, direction });

  if (!direction) {
    setSortedData(data);
    return;
  }

  const sorted = [...data].sort((a, b) => {
    const aVal = a[columnKey];
    const bVal = b[columnKey];

    if (direction === "asc") {
      return aVal > bVal ? 1 : -1;
    } else {
      return aVal < bVal ? 1 : -1;
    }
  });

  setSortedData(sorted);
};

const columns: Column[] = [
  {
    key: "name",
    dataIndex: "name",
    title: "Name",
    width: 150,
    sortable: true,
  },
  // ... more columns
];

<VirtualGrid
  dataSource={sortedData}
  columns={columns}
  sortState={sortState}
  onSort={handleSort}
/>;
```

### Filtering

```tsx
const [filters, setFilters] = useState<
  Record<string, Set<string | number | boolean>>
>({});
const [filteredData, setFilteredData] = useState(data);

const handleFiltersChange = (
  nextFilters: Record<string, Set<string | number | boolean>>
) => {
  setFilters(nextFilters);

  let filtered = [...data];

  Object.entries(nextFilters).forEach(([columnKey, filterSet]) => {
    if (filterSet.size > 0) {
      filtered = filtered.filter((row) => filterSet.has(row[columnKey]));
    }
  });

  setFilteredData(filtered);
};

const columns: Column[] = [
  {
    key: "department",
    dataIndex: "department",
    title: "Department",
    width: 150,
    filterable: true,
  },
  // ... more columns
];

<VirtualGrid
  dataSource={filteredData}
  originalDataSource={data}
  columns={columns}
  filters={filters}
  onFiltersChange={handleFiltersChange}
/>;
```

### Row Selection

```tsx
const [selectedKeys, setSelectedKeys] = useState<Array<string | number>>([]);

const handleSelectionChange = (
  keys: Array<string | number>,
  rows: Record<string, unknown>[]
) => {
  setSelectedKeys(keys);
  console.log(`${keys.length} rows selected`);
};

<VirtualGrid
  dataSource={data}
  columns={columns}
  enableRowSelection
  rowKey="id"
  selectedRowKeys={selectedKeys}
  onRowSelectionChange={handleSelectionChange}
/>;
```

### Cell Selection

```tsx
const handleCellSelection = (cells: CellSelection[]) => {
  console.log("Selected cells:", cells);
};

<VirtualGrid
  dataSource={data}
  columns={columns}
  enableCellSelection
  onCellSelection={handleCellSelection}
/>;
```

### Inline Editing

```tsx
const [dataSource, setDataSource] = useState(data);

const handleCellEdit = (params: {
  rowIndex: number;
  rowKeyValue: string | number;
  columnKey: string;
  dataIndex: string;
  value: unknown;
}) => {
  const updated = [...dataSource];
  updated[params.rowIndex][params.dataIndex] = params.value;
  setDataSource(updated);
};

const columns: Column[] = [
  {
    key: "name",
    dataIndex: "name",
    title: "Name",
    width: 150,
    editable: true,
    editor: { type: "text" },
  },
  {
    key: "age",
    dataIndex: "age",
    title: "Age",
    width: 80,
    editable: true,
    editor: { type: "number" },
  },
  {
    key: "status",
    dataIndex: "status",
    title: "Status",
    width: 120,
    editable: true,
    editor: {
      type: "select",
      options: [
        { label: "Active", value: "active" },
        { label: "Inactive", value: "inactive" },
      ],
    },
  },
];

<VirtualGrid
  dataSource={dataSource}
  columns={columns}
  onCellEdit={handleCellEdit}
/>;
```

### Column Resize

```tsx
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});

const handleResize = (columnKey: string, newWidth: number) => {
  setColumnWidths((prev) => ({
    ...prev,
    [columnKey]: newWidth,
  }));
};

<VirtualGrid
  dataSource={data}
  columns={columns}
  enableColumnResize
  columnWidths={columnWidths}
  onColumnResize={handleResize}
/>;
```

### Column Reorder

```tsx
const [columns, setColumns] = useState<Column[]>(initialColumns);

const handleReorder = (sourceIndex: number, targetIndex: number) => {
  const newColumns = [...columns];
  const [removed] = newColumns.splice(sourceIndex, 1);
  newColumns.splice(targetIndex, 0, removed);
  setColumns(newColumns);
};

<VirtualGrid
  dataSource={data}
  columns={columns}
  enableColumnReorder
  onColumnReorder={handleReorder}
/>;
```

### Custom Cell Rendering

```tsx
const columns: Column[] = [
  {
    key: "status",
    dataIndex: "status",
    title: "Status",
    width: 120,
    render: (value) => (
      <span
        style={{
          padding: "4px 8px",
          borderRadius: "4px",
          background: value === "active" ? "#52c41a" : "#ff4d4f",
          color: "white",
        }}
      >
        {value}
      </span>
    ),
  },
  {
    key: "avatar",
    dataIndex: "avatar",
    title: "Avatar",
    width: 80,
    render: (value, record) => (
      <img
        src={value}
        alt={record.name}
        style={{ width: 40, height: 40, borderRadius: "50%" }}
      />
    ),
  },
];
```

### Theming

```tsx
const darkTheme = {
  headerBackground: "#1a1a1a",
  headerColor: "#ffffff",
  rowBackground: "#141414",
  rowAlternateBackground: "#1f1f1f",
  rowHoverBackground: "#262626",
  borderColor: "#303030",
  fontSize: "14px",
  fontFamily: "system-ui, sans-serif",
  rowColor: "#ffffff",
};

<VirtualGrid dataSource={data} columns={columns} theme={darkTheme} />;
```

### Combining Multiple Features

```tsx
<VirtualGrid
  dataSource={data}
  columns={columns}
  enableRowSelection
  enableCellSelection
  enableColumnResize
  enableColumnReorder
  rowKey="id"
  sortState={sortState}
  onSort={handleSort}
  filters={filters}
  onFiltersChange={handleFiltersChange}
  selectedRowKeys={selectedKeys}
  onRowSelectionChange={handleSelectionChange}
  columnWidths={columnWidths}
  onColumnResize={handleResize}
  onColumnReorder={handleReorder}
  onCellEdit={handleCellEdit}
  theme={customTheme}
/>
```

## Performance Tips

1. **Use `rowKey`** for stable row identification
2. **Memoize columns** if they're computed
3. **Use `originalDataSource`** for filtering to show all possible filter values
4. **Avoid unnecessary re-renders** by memoizing callbacks
5. **Adjust `overscan`** based on your needs (lower = faster, higher = smoother)

## Browser Support

- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)

## TypeScript

This package is written in TypeScript and includes full type definitions. No additional `@types` package needed.

## Publishing to NPM

Before publishing, make sure to:

1. **Update package name** in `package.json` (set to `@mayur.sarvadhi/virtual-grid` using your npm username)
2. **Update repository URL** in `package.json` if you have a GitHub repository
3. **Update author** field in `package.json`
4. **Build the package**:
   ```bash
   npm run build
   ```
5. **Test the build** locally if needed

### Publishing as Private First (Then Make Public Later)

If you want to publish the package privately first and make it public later:

#### Step 1: Publish as Private/Restricted

For scoped packages (packages starting with `@`), you can publish with restricted access:

```bash
npm login
npm publish --access restricted
```

**What this does:**

- Package is published to npm registry
- Only visible/searchable by you and users you grant access to
- Requires npm paid plan for truly private packages (free tier allows restricted access for scoped packages)
- Others cannot install it without your permission

**Alternative: Keep Package Local (Not Published Yet)**

If you want to keep it completely private and not publish yet, add this to your `package.json`:

```json
{
  "private": true,
  ...
}
```

This prevents accidental publishing. Remove it when ready to publish.

#### Step 2: Make It Public Later

When you're ready to make your package public, you have two options:

**Option A: Change access of existing package**

```bash
npm access public @mayur.sarvadhi/virtual-grid
```

**Option B: Publish new version as public**

```bash
npm publish --access public
```

### Publishing Directly as Public

If you want to publish directly as public:

```bash
npm login
npm publish --access public
```

**Note:** Once published as public, anyone can see and install your package. You can always unpublish within 72 hours, but it's better to start private if you're unsure.

### Recommended Workflow

1. **Development Phase**:

   - Add `"private": true` to `package.json` OR use `--access restricted`
   - Test and iterate on the package
   - Build and test locally

2. **Private Publishing Phase**:

   - Remove `"private": true` if you added it
   - Publish with: `npm publish --access restricted`
   - Share with specific team members if needed

3. **Going Public**:
   - When ready: `npm access public @mayur.sarvadhi/virtual-grid`
   - Or publish next version: `npm publish --access public`

## License

MIT

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## Support

For issues, questions, or contributions, please open an issue on GitHub.