Official React packages for Highcharts Grid Lite and Highcharts Grid Pro. Built for React patterns, with a JSX-native API, TypeScript types, and the Grid CSS included.
+
+
+
+
+
+
+
+This is the working repository for the Grid React packages. If you want to use Grid in a React app, install a distribution package from npm rather than this repo.
+
+## Why Highcharts Grid React?
+
+- **Options or JSX** - Pass a Grid `options` object, compose with React components such as `Data`, `Column`, `Caption`, and `Pagination`, or mix both
+- **Lite and Pro** - Start with free Grid Lite, or use Grid Pro for editing, validation, sparklines, and events
+- **Self-Contained Packages** - Grid setup, cleanup, and CSS are handled for you
+- **Built for Large Tables** - Row virtualization keeps scrolling smooth with thousands of records
+- **Accessibility First** - Semantic HTML tables with keyboard navigation and screen reader support
+- **TypeScript Ready** - First-class types for options, refs, events, and component props
-This monorepo contains the following packages:
+## Packages
### Published Packages
@@ -15,9 +36,7 @@ This monorepo contains the following packages:
- **[@highcharts/grid-shared-react](./packages/grid-shared-react/)** - Shared core functionality used by both Grid Lite and Grid Pro React packages
-## Quick Start
-
-### Installation
+## Installation
```bash
# For Grid Lite
@@ -27,17 +46,24 @@ npm install @highcharts/grid-lite-react
npm install @highcharts/grid-pro-react
```
-### Usage
+> **Note:** The matching Grid Core package is included as a dependency. `react` and `react-dom` are peer dependencies and are installed automatically with npm v7+. Requires React 18 or higher.
+
+## Quick Start
+
+Components are optional. You can pass a Grid `options` object to `` the same way as before, use JSX components, or mix both.
-#### Grid Lite
+### Grid Lite
-```tsx
-import React, { useState } from 'react';
+Using options:
+
+```jsx
+import { useState } from 'react';
import { Grid, type GridOptions } from '@highcharts/grid-lite-react';
-function App() {
+export function App() {
const [options] = useState({
- dataTable: {
+ caption: { text: 'Team directory' },
+ data: {
columns: {
name: ['Alice', 'Bob', 'Charlie'],
age: [23, 34, 45]
@@ -49,15 +75,40 @@ function App() {
}
```
-#### Grid Pro
+Using components:
+
+```jsx
+import { Grid, Caption, Data, Column } from '@highcharts/grid-lite-react';
+
+export function App() {
+ return (
+
+
Highcharts Grid Lite for React makes integrating interactive data tables into your React projects intuitive and aligned with your React workflow, built with an API refined for React patterns.
+
+
+
+
+
+
+
+## Why Highcharts Grid Lite React?
+
+- **Options or JSX** - Pass a Grid `options` object, compose with React components such as `Data`, `Column`, `Caption`, and `Pagination`, or mix both
+- **Self-Contained Package** - Grid setup, cleanup, and CSS are handled for you
+- **Built for Large Tables** - Row virtualization keeps scrolling smooth with thousands of records
+- **Interactive by Default** - Sorting, filtering, and pagination without extra libraries
+- **Accessibility First** - Renders a semantic HTML table with keyboard navigation and screen reader support
+- **CSS Theming** - Customize appearance with CSS variables and class names that fit your app
+- **TypeScript Ready** - First-class types for options, refs, and component props
+
+## License
+
+Highcharts Grid Lite is free to use. Review the license terms at the links below:
+
+- [Standard License Terms](https://www.highcharts.com/license)
+- [Product page](https://www.highcharts.com/products/grid)
+
+Need editing, validation, sparklines, or events? See [@highcharts/grid-pro-react](https://www.npmjs.com/package/@highcharts/grid-pro-react).
## Installation
+Install Highcharts Grid Lite React from npm:
+
```bash
npm install @highcharts/grid-lite-react
```
-## Requirements
+Or using yarn:
-- React 18 or higher
+```bash
+yarn add @highcharts/grid-lite-react
+```
+
+> **Note:** `@highcharts/grid-lite` is included as a dependency. `react` and `react-dom` are peer dependencies and are installed automatically with npm v7+. Requires React 18 or higher.
## Quick Start
-```tsx
-import React, { useState } from 'react';
+Components are optional. You can pass a Grid `options` object to `` the same way as before, use JSX components, or mix both.
+
+### Using options
+
+```jsx
+import { useState } from 'react';
import { Grid, type GridOptions } from '@highcharts/grid-lite-react';
-function App() {
+export function App() {
const [options] = useState({
- dataTable: {
+ caption: { text: 'Team directory' },
+ data: {
columns: {
name: ['Alice', 'Bob', 'Charlie'],
age: [23, 34, 45],
city: ['New York', 'Oslo', 'Paris']
}
- },
- caption: {
- text: 'My Grid'
}
});
@@ -47,90 +75,110 @@ function App() {
}
```
-## API
-
-### `Grid`
-
-React component that wraps Highcharts Grid Lite.
-
-#### Props
-
-- `options` (required): Configuration options for the grid. Type: `GridOptions`
-- `gridRef` (optional): React ref to access the underlying grid instance. Type: `RefObject>`
-- `callback` (optional): Callback function called when the grid is initialized. Receives the grid instance as parameter. Type: `(grid: GridInstance) => void`
+### Using components
-### `GridOptions`
+```jsx
+import { Grid, Caption, Data, Column, Pagination } from '@highcharts/grid-lite-react';
-Type exported from the package for TypeScript support.
-
-```tsx
-import type { GridOptions } from '@highcharts/grid-lite-react';
+export function App() {
+ return (
+
+
Team directory
+
+
+
+
+
+
+ );
+}
```
-### `GridRefHandle`
-
-Type for the gridRef handle that provides access to the underlying grid instance.
+## Grid props
-```tsx
-import type { GridRefHandle } from '@highcharts/grid-lite-react';
+The grid is rendered inside a container. You can pass layout and theme props directly to `Grid`:
-const gridRef = useRef | null>(null);
-// Access the grid instance via gridRef.current?.grid
+```jsx
+
+
Full-width grid
+
+
```
-### `GridInstance`
+- `className` applies to the React mount container
+- `tableClassName` applies to the rendered table
+- `theme` sets the Grid theme (`rendering.theme`)
-Type for the grid instance returned by gridRef or callback.
+## TypeScript
-```tsx
-import type { GridInstance } from '@highcharts/grid-lite-react';
-```
-
-### Using gridRef and Callback
-
-You can access the grid instance in two ways:
+Use `GridOptions` for the `Grid` component `options` prop.
-**Using gridRef:**
```tsx
-import { useRef } from 'react';
-import { Grid, type GridRefHandle, type GridOptions } from '@highcharts/grid-lite-react';
+import { useState } from 'react';
+import { Grid, type GridOptions } from '@highcharts/grid-lite-react';
-function App() {
- const gridRef = useRef | null>(null);
-
- const handleClick = () => {
- // Access the grid instance
- const gridInstance = gridRef.current?.grid;
- if (gridInstance) {
- console.log('Grid instance:', gridInstance);
+export function App() {
+ const [options] = useState({
+ data: {
+ columns: {
+ name: ['Alice', 'Bob', 'Charlie'],
+ age: [23, 34, 45]
+ }
}
- };
+ });
- return (
- <>
-
-
- >
- );
+ return ;
}
```
-**Using callback:**
+Use `GridRefHandle` and `GridInstance` when you need access to the underlying Grid instance.
+
```tsx
-import { Grid, type GridInstance, type GridOptions } from '@highcharts/grid-lite-react';
+import { useRef } from 'react';
+import {
+ Grid,
+ type GridOptions,
+ type GridRefHandle,
+ type GridInstance
+} from '@highcharts/grid-lite-react';
+
+export function App() {
+ const gridRef = useRef | null>(null);
-function App() {
- const handleGridReady = (grid: GridInstance) => {
- console.log('Grid initialized:', grid);
+ const onGridReady = (grid: GridInstance) => {
+ console.log('Grid instance:', grid);
};
- return ;
+ return (
+
+ );
}
```
-### Next.js Integration
+## Next.js
-When using this package with Next.js, you need to disable Server-Side Rendering (SSR) for the Grid component:
+Grid uses browser APIs, so it must render on the client. Use a dynamic import with SSR disabled:
```tsx
'use client';
@@ -138,9 +186,7 @@ When using this package with Next.js, you need to disable Server-Side Rendering
import { useState } from 'react';
import dynamic from 'next/dynamic';
import { type GridOptions } from '@highcharts/grid-lite-react';
-import '@highcharts/grid-lite/css/grid-lite.css';
-// Disable SSR for the Grid component
const Grid = dynamic(
() => import('@highcharts/grid-lite-react').then((mod) => mod.Grid),
{ ssr: false }
@@ -148,7 +194,7 @@ const Grid = dynamic(
export default function Page() {
const [options] = useState({
- dataTable: {
+ data: {
columns: {
name: ['Alice', 'Bob', 'Charlie'],
age: [23, 34, 45]
@@ -160,12 +206,19 @@ export default function Page() {
}
```
-**Important:** The Grid component must be rendered client-side only. Always use `dynamic` import with `ssr: false` and mark your component with `'use client'` directive.
+The React package loads Grid CSS automatically. See the [Next.js guide](https://www.highcharts.com/docs/grid/frameworks/nextjs) for more detail.
## Documentation
-For detailed documentation on available options and features, see the [Highcharts Grid Lite documentation](https://www.highcharts.com/docs/grid/general).
+For comprehensive guides and API documentation, visit the [Highcharts Grid React documentation](https://www.highcharts.com/docs/grid/frameworks/react).
-## License
+- [Grid Lite getting started](https://www.highcharts.com/docs/grid/getting-started/grid-lite)
+- [Highcharts Grid overview](https://www.highcharts.com/docs/grid/general)
+
+## Support and feedback
+
+We love to learn how you are using Highcharts, and what you would like to see from us in the future.
+
+Join our vibrant community on [GitHub](https://github.com/highcharts/grid-react), [Stack Overflow](https://stackoverflow.com/tags/highcharts/), [Discord](https://discord.com/invite/xHxxcyyy6K), and the [Highcharts Forums](https://www.highcharts.com/forum/).
-SEE LICENSE IN [LICENSE](https://github.com/highcharts/grid-react/blob/main/packages/grid-lite-react/LICENSE).
+Commercial support packages are available, see [Highcharts Advantage](https://www.highcharts.com/highcharts-advantage/).
diff --git a/packages/grid-lite-react/package.json b/packages/grid-lite-react/package.json
index 36ee2cc..a24a3d1 100644
--- a/packages/grid-lite-react/package.json
+++ b/packages/grid-lite-react/package.json
@@ -37,7 +37,7 @@
"prepublishOnly": "pnpm build"
},
"dependencies": {
- "@highcharts/grid-lite": ">=3.0.0"
+ "@highcharts/grid-lite": "3.1.0"
},
"devDependencies": {
"@highcharts/grid-shared-react": "workspace:*",
diff --git a/packages/grid-lite-react/rollup.config.js b/packages/grid-lite-react/rollup.config.js
index 24ea9c0..ad3b59f 100644
--- a/packages/grid-lite-react/rollup.config.js
+++ b/packages/grid-lite-react/rollup.config.js
@@ -6,7 +6,7 @@ const isExternal = (id) => [
'react',
'react-dom',
'@highcharts/grid-lite'
-].some(pattern => id.startsWith(pattern));
+].some((pattern) => id.startsWith(pattern)) || id.endsWith('.css');
export default [{
input: 'src/index.ts',
diff --git a/packages/grid-lite-react/src/Grid.tsx b/packages/grid-lite-react/src/Grid.tsx
index f4d21fb..d6aec44 100644
--- a/packages/grid-lite-react/src/Grid.tsx
+++ b/packages/grid-lite-react/src/Grid.tsx
@@ -9,12 +9,44 @@
import {
BaseGrid,
- GridProps
+ useDeclarativeGridOptions
} from '@highcharts/grid-shared-react';
import Grid from '@highcharts/grid-lite/es-modules/masters/grid-lite.src';
import '@highcharts/grid-lite/css/grid-lite.css';
import type { Options } from '@highcharts/grid-lite/es-modules/Grid/Core/Options';
+import type { GridProps } from '@highcharts/grid-shared-react';
+import { buildGridOptions } from './utils/buildGridOptions';
-export default function GridLite({ options, gridRef, callback }: GridProps) {
- return ;
+export default function GridLite(props: GridProps) {
+ const {
+ gridRef,
+ children,
+ options,
+ callback,
+ theme,
+ className,
+ tableClassName
+ } = props;
+ const { gridOptions, columnKey } = useDeclarativeGridOptions(
+ children,
+ options,
+ (childOptions, opts) => buildGridOptions(
+ childOptions,
+ opts,
+ theme,
+ tableClassName
+ ),
+ [theme, tableClassName]
+ );
+
+ return (
+
+ );
}
diff --git a/packages/grid-lite-react/src/index.ts b/packages/grid-lite-react/src/index.ts
index 93cb0e5..79cbac6 100644
--- a/packages/grid-lite-react/src/index.ts
+++ b/packages/grid-lite-react/src/index.ts
@@ -11,6 +11,33 @@ import GridLite from '@highcharts/grid-lite';
export { default as Grid } from './Grid';
export { default as GridLite } from './Grid';
-export type { GridInstance } from '@highcharts/grid-shared-react';
-export type { GridRefHandle } from '@highcharts/grid-shared-react';
+export {
+ Caption,
+ Data,
+ ColumnDefaults,
+ Column,
+ Description,
+ Pagination,
+ Header
+} from '@highcharts/grid-shared-react';
+export { DataTable, DataConnector } from '@highcharts/grid-lite';
+export { merge } from '@highcharts/grid-lite/es-modules/Shared/Utilities.js';
+export type {
+ GridInstance,
+ GridRefHandle,
+ CaptionProps,
+ DescriptionProps,
+ DataProps,
+ DataColumns,
+ DataColumnValue,
+ ColumnProps,
+ ColumnOptionsProps,
+ ColumnDataType,
+ ColumnSortingOrder,
+ CellValueGetterContext,
+ PaginationProps,
+ HeaderProps,
+ GroupedHeaderOptions,
+ HeaderCellAccessibilityProps
+} from '@highcharts/grid-shared-react';
export type GridOptions = GridLite.Options;
diff --git a/packages/grid-lite-react/src/utils/buildGridOptions.ts b/packages/grid-lite-react/src/utils/buildGridOptions.ts
new file mode 100644
index 0000000..5bfd183
--- /dev/null
+++ b/packages/grid-lite-react/src/utils/buildGridOptions.ts
@@ -0,0 +1,45 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import { normalizeChildOptions } from '@highcharts/grid-shared-react';
+import { merge } from '@highcharts/grid-lite/es-modules/Shared/Utilities.js';
+import type { Options } from '@highcharts/grid-lite/es-modules/Grid/Core/Options';
+
+/**
+ * Builds final Grid Lite options from raw declarative child options.
+ *
+ * `theme` → `rendering.theme`
+ * `tableClassName` → `rendering.table.className` (`.hcg-table`)
+ *
+ * `className` is React-only on the mount container
+ * (parent of `.hcg-container`).
+ */
+export function buildGridOptions(
+ childOptions: Record,
+ options?: Options,
+ theme?: string,
+ tableClassName?: string
+): Options {
+ const rendering: Record = {};
+
+ if (theme !== void 0) {
+ rendering.theme = theme;
+ }
+ if (tableClassName !== void 0) {
+ rendering.table = { className: tableClassName };
+ }
+
+ return merge(
+ normalizeChildOptions(childOptions),
+ options ?? {},
+ // Skip empty `{ rendering: {} }` so merge does not inject a blank
+ // rendering block when theme / tableClassName were omitted.
+ Object.keys(rendering).length ? { rendering } : {}
+ ) as Options;
+}
diff --git a/packages/grid-lite-react/src/__tests__/Grid.test.tsx b/packages/grid-lite-react/tests/Grid.test.tsx
similarity index 72%
rename from packages/grid-lite-react/src/__tests__/Grid.test.tsx
rename to packages/grid-lite-react/tests/Grid.test.tsx
index 770322a..140e4eb 100644
--- a/packages/grid-lite-react/src/__tests__/Grid.test.tsx
+++ b/packages/grid-lite-react/tests/Grid.test.tsx
@@ -1,5 +1,5 @@
-import { createGridTests } from '@highcharts/grid-shared-react/src/test/createGridTests';
-import { Grid, GridOptions } from '../index';
+import { createGridTests } from '@highcharts/grid-shared-react/tests/createGridTests';
+import { Grid, GridOptions } from '../src/index';
createGridTests(
'Grid Lite',
diff --git a/packages/grid-lite-react/tests/buildGridOptions.test.ts b/packages/grid-lite-react/tests/buildGridOptions.test.ts
new file mode 100644
index 0000000..6fe9dcd
--- /dev/null
+++ b/packages/grid-lite-react/tests/buildGridOptions.test.ts
@@ -0,0 +1,46 @@
+import { describe, it, expect } from 'vitest';
+import { buildGridOptions } from '../src/utils/buildGridOptions';
+
+describe('buildGridOptions theme', () => {
+ it('omits rendering.theme when theme prop is undefined', () => {
+ const options = buildGridOptions({});
+
+ expect(options.rendering?.theme).toBeUndefined();
+ });
+
+ it('passes empty theme to disable Core default', () => {
+ const options = buildGridOptions({}, void 0, '');
+
+ expect(options.rendering?.theme).toBe('');
+ });
+
+ it('passes custom theme as-is', () => {
+ const options = buildGridOptions({}, void 0, 'myTheme');
+
+ expect(options.rendering?.theme).toBe('myTheme');
+ });
+
+ it('does not put className into Core options', () => {
+ // className is React-only on the mount container
+ const options = buildGridOptions({}, void 0, 'hcg-theme-default');
+
+ expect(options.rendering?.theme).toBe('hcg-theme-default');
+ expect(
+ (options.rendering as { className?: string } | undefined)?.className
+ ).toBeUndefined();
+ });
+
+ it('maps tableClassName to rendering.table.className', () => {
+ const options = buildGridOptions(
+ {},
+ void 0,
+ '',
+ 'border border-slate-300 w-full'
+ );
+
+ expect(options.rendering?.theme).toBe('');
+ expect(options.rendering?.table?.className).toBe(
+ 'border border-slate-300 w-full'
+ );
+ });
+});
diff --git a/packages/grid-pro-react/README.md b/packages/grid-pro-react/README.md
index c5848f5..900396c 100644
--- a/packages/grid-pro-react/README.md
+++ b/packages/grid-pro-react/README.md
@@ -1,135 +1,198 @@
-# @highcharts/grid-pro-react
+# Highcharts Grid Pro React
-React integration for [Highcharts Grid Pro](https://www.highcharts.com/docs/grid/general).
+
Highcharts Grid Pro for React makes integrating editable, interactive data tables into your React projects intuitive and aligned with your React workflow, built with an API refined for React patterns.
+
+
+
+
+
+
+
+## Why Highcharts Grid Pro React?
+
+- **Options or JSX** - Pass a Grid `options` object, compose with React components such as `Data`, `Column`, `Caption`, and `Pagination`, or mix both
+- **Everything in Grid Lite** - Sorting, filtering, pagination, virtualization, theming, and accessibility
+- **Interactive Data Editing** - Built-in editors for text, numbers, dates, and more
+- **Validation** - Keep data clean with configurable rules and custom business logic
+- **Sparklines** - Show trends in-cell, including Highcharts-powered visualizations
+- **React Event Props** - Hook into load, update, sort, click, and pagination events with `on*` props
+- **TypeScript Ready** - First-class types for options, refs, events, and component props
+
+## License
+
+Grid Pro is a commercial product. Getting licensed for commercial use makes you production-ready: license, updates and support for business-critical grids. To learn more, please contact our sales team at sales@highcharts.com. You can also review our Standard License Terms and our Annual License at the links below:
+
+- [Standard License Terms](https://www.highcharts.com/license)
+- [Terms & Conditions for Annual Subscription](https://shop.highcharts.com/license-annual-3.0)
+- [Product page](https://www.highcharts.com/products/grid)
+
+Looking for the free edition? See [@highcharts/grid-lite-react](https://www.npmjs.com/package/@highcharts/grid-lite-react).
## Installation
+Install Highcharts Grid Pro React from npm:
+
```bash
npm install @highcharts/grid-pro-react
```
-## Requirements
+Or using yarn:
-- React 18 or higher
+```bash
+yarn add @highcharts/grid-pro-react
+```
+
+> **Note:** `@highcharts/grid-pro` is included as a dependency. `react` and `react-dom` are peer dependencies and are installed automatically with npm v7+. Requires React 18 or higher.
## Quick Start
-```tsx
-import React, { useState } from 'react';
+Pass your Grid Pro license key with `gridKey`. Components are optional. You can pass a Grid `options` object to `` the same way as before, use JSX components, or mix both.
+
+### Using options
+
+```jsx
+import { useState } from 'react';
import { Grid, type GridOptions } from '@highcharts/grid-pro-react';
-function App() {
+export function App() {
const [options] = useState({
- dataTable: {
+ caption: { text: 'Team directory' },
+ data: {
columns: {
name: ['Alice', 'Bob', 'Charlie'],
age: [23, 34, 45],
city: ['New York', 'Oslo', 'Paris']
}
- },
- caption: {
- text: 'My Grid'
}
});
- return ;
+ return ;
}
```
-## API
-
-### `Grid`
-
-React component that wraps Highcharts Grid Pro.
-
-#### Props
-
-- `options` (required): Configuration options for the grid. Type: `GridOptions`
-- `gridRef` (optional): React ref to access the underlying grid instance. Type: `RefObject>`
-- `callback` (optional): Callback function called when the grid is initialized. Receives the grid instance as parameter. Type: `(grid: GridInstance) => void`
+### Using components
-### `GridOptions`
+```jsx
+import {
+ Grid,
+ Caption,
+ Data,
+ Column,
+ Pagination
+} from '@highcharts/grid-pro-react';
-Type exported from the package for TypeScript support.
-
-```tsx
-import type { GridOptions } from '@highcharts/grid-pro-react';
+export function App() {
+ return (
+
+
Team directory
+
+
+
+
+
+
+ );
+}
```
-### `GridRefHandle`
-
-Type for the gridRef handle that provides access to the underlying grid instance.
-
-```tsx
-import type { GridRefHandle } from '@highcharts/grid-pro-react';
-
-const gridRef = useRef | null>(null);
-// Access the grid instance via gridRef.current?.grid
+## Grid props
+
+The grid is rendered inside a container. You can pass layout, theme, and Pro event props directly to `Grid`:
+
+```jsx
+
+
Full-width grid
+
+
```
-### `GridInstance`
+- `gridKey` is required and sets your Grid Pro license key
+- `className` applies to the React mount container
+- `tableClassName` applies to the rendered table
+- `theme` sets the Grid theme (`rendering.theme`)
+- `onAfterLoad` and other `on*` props map to Grid Pro events
-Type for the grid instance returned by gridRef or callback.
+## TypeScript
-```tsx
-import type { GridInstance } from '@highcharts/grid-pro-react';
-```
+Use `GridOptions` for the `Grid` component `options` prop.
-### Using gridRef and Callback
-
-You can access the grid instance in two ways:
-
-**Using gridRef:**
```tsx
-import { useRef } from 'react';
-import { Grid, type GridRefHandle, type GridOptions } from '@highcharts/grid-pro-react';
+import { useState } from 'react';
+import { Grid, type GridOptions } from '@highcharts/grid-pro-react';
-function App() {
- const gridRef = useRef | null>(null);
-
- const handleClick = () => {
- // Access the grid instance
- const gridInstance = gridRef.current?.grid;
- if (gridInstance) {
- console.log('Grid instance:', gridInstance);
+export function App() {
+ const [options] = useState({
+ data: {
+ columns: {
+ name: ['Alice', 'Bob', 'Charlie'],
+ age: [23, 34, 45]
+ }
}
- };
+ });
- return (
- <>
-
-
- >
- );
+ return ;
}
```
-**Using callback:**
+Use `GridRefHandle` and `GridInstance` when you need access to the underlying Grid instance.
+
```tsx
-import { Grid, type GridInstance, type GridOptions } from '@highcharts/grid-pro-react';
+import { useRef } from 'react';
+import {
+ Grid,
+ type GridOptions,
+ type GridRefHandle,
+ type GridInstance
+} from '@highcharts/grid-pro-react';
+
+export function App() {
+ const gridRef = useRef | null>(null);
-function App() {
- const handleGridReady = (grid: GridInstance) => {
- console.log('Grid initialized:', grid);
+ const onGridReady = (grid: GridInstance) => {
+ console.log('Grid instance:', grid);
};
- return ;
+ return (
+
+ );
}
```
-### Next.js Integration
+## Next.js
-When using this package with Next.js, you need to disable Server-Side Rendering (SSR) for the Grid component:
+Grid uses browser APIs, so it must render on the client. Use a dynamic import with SSR disabled:
```tsx
'use client';
@@ -137,9 +200,7 @@ When using this package with Next.js, you need to disable Server-Side Rendering
import { useState } from 'react';
import dynamic from 'next/dynamic';
import { type GridOptions } from '@highcharts/grid-pro-react';
-import '@highcharts/grid-pro/css/grid-pro.css';
-// Disable SSR for the Grid component
const Grid = dynamic(
() => import('@highcharts/grid-pro-react').then((mod) => mod.Grid),
{ ssr: false }
@@ -147,7 +208,7 @@ const Grid = dynamic(
export default function Page() {
const [options] = useState({
- dataTable: {
+ data: {
columns: {
name: ['Alice', 'Bob', 'Charlie'],
age: [23, 34, 45]
@@ -155,16 +216,23 @@ export default function Page() {
}
});
- return ;
+ return ;
}
```
-**Important:** The Grid component must be rendered client-side only. Always use `dynamic` import with `ssr: false` and mark your component with `'use client'` directive.
+The React package loads Grid CSS automatically. See the [Next.js guide](https://www.highcharts.com/docs/grid/frameworks/nextjs) for more detail.
## Documentation
-For detailed documentation on available options and features, see the [Highcharts Grid Pro documentation](https://www.highcharts.com/docs/grid/general).
+For comprehensive guides and API documentation, visit the [Highcharts Grid React documentation](https://www.highcharts.com/docs/grid/frameworks/react).
-## License
+- [Grid Pro getting started](https://www.highcharts.com/docs/grid/getting-started/grid-pro)
+- [Highcharts Grid overview](https://www.highcharts.com/docs/grid/general)
+
+## Support and feedback
+
+We love to learn how you are using Highcharts, and what you would like to see from us in the future.
+
+Join our vibrant community on [GitHub](https://github.com/highcharts/grid-react), [Stack Overflow](https://stackoverflow.com/tags/highcharts/), [Discord](https://discord.com/invite/xHxxcyyy6K), and the [Highcharts Forums](https://www.highcharts.com/forum/).
-SEE LICENSE IN [LICENSE](https://github.com/highcharts/grid-react/blob/main/packages/grid-pro-react/LICENSE).
+Commercial support packages are available, see [Highcharts Advantage](https://www.highcharts.com/highcharts-advantage/).
diff --git a/packages/grid-pro-react/package.json b/packages/grid-pro-react/package.json
index 7012236..ef043ee 100644
--- a/packages/grid-pro-react/package.json
+++ b/packages/grid-pro-react/package.json
@@ -38,7 +38,7 @@
"prepublishOnly": "pnpm build"
},
"dependencies": {
- "@highcharts/grid-pro": ">=3.0.0"
+ "@highcharts/grid-pro": "3.1.0"
},
"devDependencies": {
"@highcharts/grid-shared-react": "workspace:*",
diff --git a/packages/grid-pro-react/src/Grid.tsx b/packages/grid-pro-react/src/Grid.tsx
index 398edea..7400e61 100644
--- a/packages/grid-pro-react/src/Grid.tsx
+++ b/packages/grid-pro-react/src/Grid.tsx
@@ -9,12 +9,38 @@
import {
BaseGrid,
- GridProps
+ useDeclarativeGridOptions
} from '@highcharts/grid-shared-react';
import Grid from '@highcharts/grid-pro/es-modules/masters/grid-pro.src';
import '@highcharts/grid-pro/css/grid-pro.css';
-import type { Options } from '@highcharts/grid-pro/es-modules/Grid/Core/Options';
+import type { GridProProps } from './utils/mappers/grid';
+import {
+ getGridEventPropDeps
+} from './utils/mappers/grid';
+import { buildGridOptions } from './utils/buildGridOptions';
+
+export default function GridPro(props: GridProProps) {
+ const { gridRef, children, options, callback, className } = props;
+ const { gridOptions, columnKey } = useDeclarativeGridOptions(
+ children,
+ options,
+ (childOptions, opts) => buildGridOptions(
+ props.gridKey,
+ childOptions,
+ opts,
+ props
+ ),
+ getGridEventPropDeps(props)
+ );
-export default function GridPro({ options, gridRef, callback }: GridProps) {
- return ;
+ return (
+
+ );
}
diff --git a/packages/grid-pro-react/src/index.ts b/packages/grid-pro-react/src/index.ts
index a44be66..01e6fee 100644
--- a/packages/grid-pro-react/src/index.ts
+++ b/packages/grid-pro-react/src/index.ts
@@ -7,10 +7,59 @@
*
*/
-import GridPro from '@highcharts/grid-pro';
+import type { ComponentType } from 'react';
+import {
+ Column as SharedColumn,
+ Data as SharedData,
+ Pagination as SharedPagination,
+ Caption,
+ ColumnDefaults,
+ Description,
+ Header
+} from '@highcharts/grid-shared-react';
+import type { ProColumnProps } from './utils/mappers/column';
+import type { ProPaginationProps } from './utils/mappers/pagination';
export { default as Grid } from './Grid';
export { default as GridPro } from './Grid';
-export type { GridInstance } from '@highcharts/grid-shared-react';
-export type { GridRefHandle } from '@highcharts/grid-shared-react';
-export type GridOptions = GridPro.Options;
+export { Caption, ColumnDefaults, Description, Header };
+export const Column = SharedColumn as ComponentType;
+export const Data = SharedData;
+export const Pagination = SharedPagination as ComponentType;
+export { DataTable, DataConnector } from '@highcharts/grid-pro';
+export { merge } from '@highcharts/grid-pro/es-modules/Shared/Utilities.js';
+export type {
+ GridInstance,
+ GridRefHandle,
+ CaptionProps,
+ DescriptionProps,
+ DataProps,
+ DataColumns,
+ DataColumnValue,
+ ColumnOptionsProps,
+ ColumnDataType,
+ ColumnSortingOrder,
+ CellValueGetterContext,
+ HeaderProps,
+ GroupedHeaderOptions,
+ HeaderCellAccessibilityProps
+} from '@highcharts/grid-shared-react';
+export type {
+ GridProProps,
+ GridProOptions,
+ GridOptions,
+ GridEventProps,
+ GridLevelEventProps,
+ RowPinningEventProps
+} from './utils/mappers/grid';
+export type {
+ ProColumnProps,
+ ProColumnEventProps,
+ ColumnLevelEventProps,
+ CellLevelEventProps,
+ HeaderLevelEventProps
+} from './utils/mappers/column';
+export type {
+ ProPaginationProps,
+ PaginationEventProps
+} from './utils/mappers/pagination';
diff --git a/packages/grid-pro-react/src/utils/buildGridOptions.ts b/packages/grid-pro-react/src/utils/buildGridOptions.ts
new file mode 100644
index 0000000..107b096
--- /dev/null
+++ b/packages/grid-pro-react/src/utils/buildGridOptions.ts
@@ -0,0 +1,60 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import { normalizeChildOptions } from '@highcharts/grid-shared-react';
+import { merge } from '@highcharts/grid-pro/es-modules/Shared/Utilities.js';
+import { mergeColumnEventProps } from './mappers/column';
+import { mergePaginationEventProps } from './mappers/pagination';
+import {
+ normalizeGridEventProps,
+ type GridProOptions,
+ type GridProProps
+} from './mappers/grid/gridOptions';
+
+/**
+ * Builds final Grid Pro options from raw declarative child options.
+ *
+ * `theme` → `rendering.theme`
+ * `tableClassName` → `rendering.table.className` (`.hcg-table`)
+ *
+ * `className` is React-only on the mount container
+ * (parent of `.hcg-container`).
+ */
+export function buildGridOptions(
+ gridKey: string,
+ childOptions: Record,
+ options: GridProOptions | undefined,
+ props: GridProProps
+): GridProOptions {
+ const declarativeOptions = mergePaginationEventProps(
+ mergeColumnEventProps(normalizeChildOptions(childOptions))
+ );
+ const rendering: Record = {};
+
+ if (props.theme !== void 0) {
+ rendering.theme = props.theme;
+ }
+ if (props.tableClassName !== void 0) {
+ rendering.table = { className: props.tableClassName };
+ }
+
+ const result = merge(
+ true,
+ {},
+ merge(declarativeOptions, options ?? {}),
+ // Skip empty `{ rendering: {} }` so merge does not inject a blank
+ // rendering block when theme / tableClassName were omitted.
+ Object.keys(rendering).length ? { rendering } : {},
+ normalizeGridEventProps(props)
+ ) as GridProOptions;
+
+ result.gridKey = gridKey;
+
+ return result;
+}
diff --git a/packages/grid-pro-react/src/utils/mapEventsProps.ts b/packages/grid-pro-react/src/utils/mapEventsProps.ts
new file mode 100644
index 0000000..0da23d7
--- /dev/null
+++ b/packages/grid-pro-react/src/utils/mapEventsProps.ts
@@ -0,0 +1,81 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import { isObject } from '@highcharts/grid-shared-react';
+
+function setNestedValue(
+ target: Record,
+ path: readonly string[],
+ value: unknown
+): void {
+ if (path.length === 0) {
+ return;
+ }
+
+ let current = target;
+
+ for (let i = 0; i < path.length - 1; i++) {
+ const key = path[i];
+
+ if (key === void 0) {
+ continue;
+ }
+
+ const next = current[key];
+
+ if (!isObject(next)) {
+ current[key] = {};
+ } else {
+ current[key] = { ...next };
+ }
+
+ current = current[key] as Record;
+ }
+
+ const lastKey = path.at(-1);
+
+ if (lastKey !== void 0) {
+ current[lastKey] = value;
+ }
+}
+
+/**
+ * Maps flat event props onto nested option paths.
+ *
+ * When `source` is omitted, handlers are read from `target` and flat props
+ * are removed after mapping. When `source` is provided, handlers are copied
+ * onto `target` without mutating `source`.
+ *
+ * @example
+ * mapEventsProps(column, {
+ * onCellClick: ['cells', 'events', 'click']
+ * });
+ */
+export function mapEventsProps(
+ target: Record,
+ aliases: Record,
+ source?: Record
+): void {
+ const props = source ?? target;
+ const removeFlatProps = source === void 0;
+
+ for (const [propName, path] of Object.entries(aliases)) {
+ const handler = props[propName];
+
+ if (typeof handler !== 'function') {
+ continue;
+ }
+
+ if (removeFlatProps) {
+ delete target[propName];
+ }
+
+ setNestedValue(target, path, handler);
+ }
+}
diff --git a/packages/grid-pro-react/src/utils/mappers/column/columnOptions.ts b/packages/grid-pro-react/src/utils/mappers/column/columnOptions.ts
new file mode 100644
index 0000000..a1e3bce
--- /dev/null
+++ b/packages/grid-pro-react/src/utils/mappers/column/columnOptions.ts
@@ -0,0 +1,108 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import { isObject } from '@highcharts/grid-shared-react';
+import { mapEventsProps } from '../../mapEventsProps';
+import type { ColumnProps } from '@highcharts/grid-shared-react';
+import type {
+ CellEventCallback,
+ ColumnEventCallback
+} from '@highcharts/grid-pro/es-modules/Grid/Pro/GridEvents.js';
+
+/**
+ * Column-level event props mapped to `columns[].events`.
+ */
+export interface ColumnLevelEventProps {
+ onAfterResize?: ColumnEventCallback;
+ onBeforeSort?: ColumnEventCallback;
+ onAfterSort?: ColumnEventCallback;
+ onBeforeFilter?: ColumnEventCallback;
+ onAfterFilter?: ColumnEventCallback;
+}
+
+/**
+ * Cell-level event props mapped to `columns[].cells.events`.
+ */
+export interface CellLevelEventProps {
+ onCellClick?: CellEventCallback;
+ onCellDblClick?: CellEventCallback;
+ onCellMouseOver?: CellEventCallback;
+ onCellMouseOut?: CellEventCallback;
+ onCellAfterRender?: CellEventCallback;
+ onCellAfterEdit?: CellEventCallback;
+}
+
+/**
+ * Header-level event props mapped to `columns[].header.events`.
+ */
+export interface HeaderLevelEventProps {
+ onHeaderClick?: ColumnEventCallback;
+ onHeaderAfterRender?: ColumnEventCallback;
+}
+
+export type ProColumnEventProps = (
+ ColumnLevelEventProps &
+ CellLevelEventProps &
+ HeaderLevelEventProps
+);
+
+/**
+ * Column props for Grid Pro, including event handlers.
+ */
+export type ProColumnProps = ColumnProps & ProColumnEventProps;
+
+/** Flat event prop → nested Grid option path for columns. */
+const COLUMN_EVENT_ALIASES = {
+ onAfterResize: ['events', 'afterResize'],
+ onBeforeSort: ['events', 'beforeSort'],
+ onAfterSort: ['events', 'afterSort'],
+ onBeforeFilter: ['events', 'beforeFilter'],
+ onAfterFilter: ['events', 'afterFilter'],
+ onCellClick: ['cells', 'events', 'click'],
+ onCellDblClick: ['cells', 'events', 'dblClick'],
+ onCellMouseOver: ['cells', 'events', 'mouseOver'],
+ onCellMouseOut: ['cells', 'events', 'mouseOut'],
+ onCellAfterRender: ['cells', 'events', 'afterRender'],
+ onCellAfterEdit: ['cells', 'events', 'afterEdit'],
+ onHeaderClick: ['header', 'events', 'click'],
+ onHeaderAfterRender: ['header', 'events', 'afterRender']
+} as const satisfies Record;
+
+/**
+ * Maps Pro column event props onto nested Grid option paths.
+ */
+export function mapColumnEventProps(
+ props: Record
+): Record {
+ const options = { ...props };
+
+ mapEventsProps(options, COLUMN_EVENT_ALIASES);
+
+ return options;
+}
+
+/**
+ * Maps Pro column event props on declarative `options.columns`.
+ */
+export function mergeColumnEventProps(
+ options: Record
+): Record {
+ const columns = options.columns;
+
+ if (!Array.isArray(columns)) {
+ return options;
+ }
+
+ return {
+ ...options,
+ columns: columns.map((column) => (
+ isObject(column) ? mapColumnEventProps(column) : column
+ ))
+ };
+}
diff --git a/packages/grid-pro-react/src/utils/mappers/column/index.ts b/packages/grid-pro-react/src/utils/mappers/column/index.ts
new file mode 100644
index 0000000..6c7abfd
--- /dev/null
+++ b/packages/grid-pro-react/src/utils/mappers/column/index.ts
@@ -0,0 +1,21 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+export {
+ mapColumnEventProps,
+ mergeColumnEventProps
+} from './columnOptions';
+
+export type {
+ ColumnLevelEventProps,
+ CellLevelEventProps,
+ HeaderLevelEventProps,
+ ProColumnEventProps,
+ ProColumnProps
+} from './columnOptions';
diff --git a/packages/grid-pro-react/src/utils/mappers/grid/gridOptions.ts b/packages/grid-pro-react/src/utils/mappers/grid/gridOptions.ts
new file mode 100644
index 0000000..4d62502
--- /dev/null
+++ b/packages/grid-pro-react/src/utils/mappers/grid/gridOptions.ts
@@ -0,0 +1,113 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import type { GridProps as BaseGridProps } from '@highcharts/grid-shared-react';
+import type GridPro from '@highcharts/grid-pro';
+import type { GridEventCallback } from '@highcharts/grid-pro/es-modules/Grid/Pro/GridEvents.js';
+import type {
+ RowPinningChangeEventCallback
+} from '@highcharts/grid-pro/es-modules/Grid/Pro/RowPinning/RowPinningController.js';
+import type {
+ AfterTreeRowToggleEvent,
+ BeforeTreeRowToggleEvent
+} from '@highcharts/grid-pro/es-modules/Grid/Pro/TreeView/Projection/TreeProjectionController.js';
+import { mapEventsProps } from '../../mapEventsProps';
+
+/**
+ * Grid Pro options, including license key support from the Pro bundle.
+ */
+export type GridProOptions = GridPro.Options & {
+ gridKey?: string;
+};
+
+export type GridOptions = GridProOptions;
+
+/**
+ * Grid-level event props mapped to `options.events`.
+ */
+export interface GridLevelEventProps {
+ onBeforeLoad?: GridEventCallback;
+ onAfterLoad?: GridEventCallback;
+ onBeforeUpdate?: GridEventCallback;
+ onAfterUpdate?: GridEventCallback;
+ onBeforeRedraw?: GridEventCallback;
+ onAfterRedraw?: GridEventCallback;
+ onBeforeTreeRowToggle?: (e: BeforeTreeRowToggleEvent) => void;
+ onAfterTreeRowToggle?: (e: AfterTreeRowToggleEvent) => void;
+}
+
+/**
+ * Row pinning event props mapped to
+ * `options.rendering.rows.pinning.events`.
+ */
+export interface RowPinningEventProps {
+ onBeforeRowPin?: RowPinningChangeEventCallback;
+ onAfterRowPin?: RowPinningChangeEventCallback;
+}
+
+export type GridEventProps = GridLevelEventProps & RowPinningEventProps;
+
+/**
+ * Props for the Grid Pro React component.
+ */
+export interface GridProProps
+ extends BaseGridProps, GridEventProps {
+ /**
+ * Grid Pro license key.
+ */
+ gridKey: string;
+}
+
+/** Flat event prop → nested Grid option path for grid props. */
+const GRID_EVENT_ALIASES = {
+ onBeforeLoad: ['events', 'beforeLoad'],
+ onAfterLoad: ['events', 'afterLoad'],
+ onBeforeUpdate: ['events', 'beforeUpdate'],
+ onAfterUpdate: ['events', 'afterUpdate'],
+ onBeforeRedraw: ['events', 'beforeRedraw'],
+ onAfterRedraw: ['events', 'afterRedraw'],
+ onBeforeTreeRowToggle: ['events', 'beforeTreeRowToggle'],
+ onAfterTreeRowToggle: ['events', 'afterTreeRowToggle'],
+ onBeforeRowPin: ['rendering', 'rows', 'pinning', 'events', 'beforeRowPin'],
+ onAfterRowPin: ['rendering', 'rows', 'pinning', 'events', 'afterRowPin']
+} as const satisfies Record;
+
+export const GRID_EVENT_PROP_KEYS = Object.keys(
+ GRID_EVENT_ALIASES
+) as (keyof GridEventProps)[];
+
+/**
+ * Maps Pro event props from declarative `` props.
+ */
+export function normalizeGridEventProps(
+ props: GridProProps
+): Record {
+ const options: Record = {};
+
+ mapEventsProps(
+ options,
+ GRID_EVENT_ALIASES,
+ props as unknown as Record
+ );
+
+ return options;
+}
+
+/**
+ * Event handler values from Grid props, for `useMemo` dependency lists.
+ */
+export function getGridEventPropDeps(props: GridProProps): unknown[] {
+ return [
+ props.gridKey,
+ props.theme,
+ props.className,
+ props.tableClassName,
+ ...GRID_EVENT_PROP_KEYS.map((key) => props[key])
+ ];
+}
diff --git a/packages/grid-pro-react/src/utils/mappers/grid/index.ts b/packages/grid-pro-react/src/utils/mappers/grid/index.ts
new file mode 100644
index 0000000..6f8427a
--- /dev/null
+++ b/packages/grid-pro-react/src/utils/mappers/grid/index.ts
@@ -0,0 +1,23 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+export {
+ normalizeGridEventProps,
+ getGridEventPropDeps,
+ GRID_EVENT_PROP_KEYS
+} from './gridOptions';
+
+export type {
+ GridProOptions,
+ GridOptions,
+ GridLevelEventProps,
+ RowPinningEventProps,
+ GridEventProps,
+ GridProProps
+} from './gridOptions';
diff --git a/packages/grid-pro-react/src/utils/mappers/pagination/index.ts b/packages/grid-pro-react/src/utils/mappers/pagination/index.ts
new file mode 100644
index 0000000..0cde40f
--- /dev/null
+++ b/packages/grid-pro-react/src/utils/mappers/pagination/index.ts
@@ -0,0 +1,18 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+export {
+ mapPaginationEventProps,
+ mergePaginationEventProps
+} from './paginationOptions';
+
+export type {
+ PaginationEventProps,
+ ProPaginationProps
+} from './paginationOptions';
diff --git a/packages/grid-pro-react/src/utils/mappers/pagination/paginationOptions.ts b/packages/grid-pro-react/src/utils/mappers/pagination/paginationOptions.ts
new file mode 100644
index 0000000..855a0c8
--- /dev/null
+++ b/packages/grid-pro-react/src/utils/mappers/pagination/paginationOptions.ts
@@ -0,0 +1,72 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import { isObject } from '@highcharts/grid-shared-react';
+import { mapEventsProps } from '../../mapEventsProps';
+import type { PaginationProps } from '@highcharts/grid-shared-react';
+import type {
+ AfterPageChangeEvent,
+ AfterPageSizeChangeEvent,
+ BeforePageChangeEvent,
+ BeforePageSizeChangeEvent
+} from '@highcharts/grid-pro/es-modules/Grid/Pro/Pagination/PaginationComposition.js';
+
+/**
+ * Pagination event props mapped to `pagination.events`.
+ */
+export interface PaginationEventProps {
+ onBeforePageChange?: (e: BeforePageChangeEvent) => void;
+ onAfterPageChange?: (e: AfterPageChangeEvent) => void;
+ onBeforePageSizeChange?: (e: BeforePageSizeChangeEvent) => void;
+ onAfterPageSizeChange?: (e: AfterPageSizeChangeEvent) => void;
+}
+
+/**
+ * Pagination props for Grid Pro, including event handlers.
+ */
+export type ProPaginationProps = PaginationProps & PaginationEventProps;
+
+/** Flat event prop → nested Grid option path for pagination. */
+const PAGINATION_EVENT_ALIASES = {
+ onBeforePageChange: ['events', 'beforePageChange'],
+ onAfterPageChange: ['events', 'afterPageChange'],
+ onBeforePageSizeChange: ['events', 'beforePageSizeChange'],
+ onAfterPageSizeChange: ['events', 'afterPageSizeChange']
+} as const satisfies Record;
+
+/**
+ * Maps Pro pagination event props onto nested Grid option paths.
+ */
+export function mapPaginationEventProps(
+ props: Record
+): Record {
+ const options = { ...props };
+
+ mapEventsProps(options, PAGINATION_EVENT_ALIASES);
+
+ return options;
+}
+
+/**
+ * Maps Pro pagination event props on declarative `options.pagination`.
+ */
+export function mergePaginationEventProps(
+ options: Record
+): Record {
+ const pagination = options.pagination;
+
+ if (!isObject(pagination)) {
+ return options;
+ }
+
+ return {
+ ...options,
+ pagination: mapPaginationEventProps({ ...pagination })
+ };
+}
diff --git a/packages/grid-pro-react/src/__tests__/Grid.test.tsx b/packages/grid-pro-react/tests/Grid.test.tsx
similarity index 54%
rename from packages/grid-pro-react/src/__tests__/Grid.test.tsx
rename to packages/grid-pro-react/tests/Grid.test.tsx
index e44d6d9..7cbdcb2 100644
--- a/packages/grid-pro-react/src/__tests__/Grid.test.tsx
+++ b/packages/grid-pro-react/tests/Grid.test.tsx
@@ -1,7 +1,9 @@
-import { createGridTests } from '@highcharts/grid-shared-react/src/test/createGridTests';
-import { Grid, GridOptions } from '../index';
+import { createGridTests } from '@highcharts/grid-shared-react/tests/createGridTests';
+import { Grid, GridOptions } from '../src/index';
-createGridTests(
+const GRID_KEY = 'AAAA-BBBB-CCCC-DDDD-EEEE-FFFF';
+
+createGridTests(
'Grid Pro',
Grid,
{
@@ -19,5 +21,8 @@ createGridTests(
age: [40, 35]
}
}
+ },
+ {
+ gridKey: GRID_KEY
}
);
diff --git a/packages/grid-pro-react/tests/mappers/columnOptions.test.tsx b/packages/grid-pro-react/tests/mappers/columnOptions.test.tsx
new file mode 100644
index 0000000..4562165
--- /dev/null
+++ b/packages/grid-pro-react/tests/mappers/columnOptions.test.tsx
@@ -0,0 +1,90 @@
+import { describe, it, expect, vi } from 'vitest';
+import { Column, Data } from '../../src/index';
+import {
+ mergeColumnEventProps,
+ mapColumnEventProps
+} from '../../src/utils/mappers/column';
+import { getChildProps, normalizeChildOptions } from '@highcharts/grid-shared-react';
+
+describe('mapColumnEventProps', () => {
+ it('maps column event props onto nested option paths', () => {
+ const onAfterSort = vi.fn();
+ const onCellClick = vi.fn();
+
+ expect(mapColumnEventProps({
+ columnId: 'name',
+ onAfterSort,
+ onCellClick
+ })).toEqual({
+ columnId: 'name',
+ events: {
+ afterSort: onAfterSort
+ },
+ cells: {
+ events: {
+ click: onCellClick
+ }
+ }
+ });
+ });
+
+ it('maps header event props onto nested option paths', () => {
+ const onHeaderClick = vi.fn();
+ const onHeaderAfterRender = vi.fn();
+
+ expect(mapColumnEventProps({
+ columnId: 'name',
+ onHeaderClick,
+ onHeaderAfterRender
+ })).toEqual({
+ columnId: 'name',
+ header: {
+ events: {
+ click: onHeaderClick,
+ afterRender: onHeaderAfterRender
+ }
+ }
+ });
+ });
+});
+
+describe('mergeColumnEventProps', () => {
+ it('maps event props on declarative column options', () => {
+ const onAfterSort = vi.fn();
+ const onCellClick = vi.fn();
+
+ const options = mergeColumnEventProps(
+ normalizeChildOptions(
+ getChildProps(
+ <>
+
+
+ >
+ )
+ )
+ );
+
+ expect(options.columns).toEqual([
+ {
+ id: 'name',
+ events: {
+ afterSort: onAfterSort
+ },
+ cells: {
+ events: {
+ click: onCellClick
+ }
+ }
+ }
+ ]);
+ });
+});
diff --git a/packages/grid-pro-react/tests/mappers/gridOptions.test.tsx b/packages/grid-pro-react/tests/mappers/gridOptions.test.tsx
new file mode 100644
index 0000000..dc07360
--- /dev/null
+++ b/packages/grid-pro-react/tests/mappers/gridOptions.test.tsx
@@ -0,0 +1,171 @@
+import { describe, it, expect, vi } from 'vitest';
+import { Column } from '../../src/index';
+import { buildGridOptions } from '../../src/utils/buildGridOptions';
+import {
+ getGridEventPropDeps,
+ GRID_EVENT_PROP_KEYS,
+ normalizeGridEventProps
+} from '../../src/utils/mappers/grid';
+import { getChildProps } from '@highcharts/grid-shared-react';
+import type { GridProOptions, GridProProps } from '../../src/utils/mappers/grid';
+
+describe('normalizeGridEventProps', () => {
+ it('maps grid-level and row pinning event props', () => {
+ const onAfterLoad = vi.fn();
+ const onAfterRowPin = vi.fn();
+
+ expect(normalizeGridEventProps({
+ gridKey: 'GRID-KEY',
+ onAfterLoad,
+ onAfterRowPin
+ } as GridProProps)).toEqual({
+ events: {
+ afterLoad: onAfterLoad
+ },
+ rendering: {
+ rows: {
+ pinning: {
+ events: {
+ afterRowPin: onAfterRowPin
+ }
+ }
+ }
+ }
+ });
+ });
+});
+
+describe('buildGridOptions', () => {
+ it('merges gridKey and grid-level events into options', () => {
+ const onAfterLoad = vi.fn();
+ const options = buildGridOptions(
+ 'GRID-KEY',
+ {
+ data: {
+ columns: {
+ name: ['Alice']
+ }
+ }
+ },
+ {
+ gridKey: 'OLD-KEY',
+ events: {
+ beforeLoad: vi.fn()
+ }
+ } as GridProOptions,
+ {
+ gridKey: 'GRID-KEY',
+ onAfterLoad
+ } as GridProProps
+ );
+
+ expect(options.gridKey).toBe('GRID-KEY');
+ expect(options.events?.beforeLoad).toBeTypeOf('function');
+ expect(options.events?.afterLoad).toBe(onAfterLoad);
+ });
+
+ it('maps declarative children and builds full grid options', () => {
+ const options = buildGridOptions(
+ 'GRID-KEY',
+ getChildProps(),
+ void 0,
+ { gridKey: 'GRID-KEY' } as GridProProps
+ );
+
+ expect(options.gridKey).toBe('GRID-KEY');
+ expect(options.columns).toEqual([{ id: 'name' }]);
+ });
+
+ it('omits rendering.theme when theme prop is undefined', () => {
+ const options = buildGridOptions(
+ 'GRID-KEY',
+ {},
+ void 0,
+ {
+ gridKey: 'GRID-KEY',
+ className: 'rounded-md border'
+ } as GridProProps
+ );
+
+ expect(options.rendering?.theme).toBeUndefined();
+ expect(
+ (options.rendering as { className?: string } | undefined)?.className
+ ).toBeUndefined();
+ });
+
+ it('passes empty theme to disable Core default', () => {
+ const options = buildGridOptions(
+ 'GRID-KEY',
+ {},
+ void 0,
+ {
+ gridKey: 'GRID-KEY',
+ theme: '',
+ className: 'rounded-md border'
+ } as GridProProps
+ );
+
+ expect(options.rendering?.theme).toBe('');
+ expect(
+ (options.rendering as { className?: string } | undefined)?.className
+ ).toBeUndefined();
+ });
+
+ it('passes custom theme without putting className into Core options', () => {
+ const options = buildGridOptions(
+ 'GRID-KEY',
+ {},
+ void 0,
+ {
+ gridKey: 'GRID-KEY',
+ theme: 'myTheme',
+ className: 'rounded-md border'
+ } as GridProProps
+ );
+
+ expect(options.rendering?.theme).toBe('myTheme');
+ expect(
+ (options.rendering as { className?: string } | undefined)?.className
+ ).toBeUndefined();
+ });
+
+ it('maps tableClassName to rendering.table.className', () => {
+ const options = buildGridOptions(
+ 'GRID-KEY',
+ {},
+ void 0,
+ {
+ gridKey: 'GRID-KEY',
+ theme: '',
+ className: 'p-8',
+ tableClassName: 'border border-slate-300'
+ } as GridProProps
+ );
+
+ expect(options.rendering?.theme).toBe('');
+ expect(options.rendering?.table?.className).toBe(
+ 'border border-slate-300'
+ );
+ });
+});
+
+describe('getGridEventPropDeps', () => {
+ it('exposes stable dep keys for every grid event prop', () => {
+ const onAfterLoad = vi.fn();
+ const props = {
+ gridKey: 'KEY',
+ onAfterLoad
+ } as GridProProps;
+
+ expect(GRID_EVENT_PROP_KEYS).toContain('onAfterLoad');
+ expect(getGridEventPropDeps(props)).toEqual([
+ 'KEY',
+ void 0,
+ void 0,
+ void 0,
+ ...GRID_EVENT_PROP_KEYS.map(
+ (key: keyof GridProProps) => props[key]
+ )
+ ]);
+ });
+});
diff --git a/packages/grid-pro-react/tests/mappers/paginationOptions.test.tsx b/packages/grid-pro-react/tests/mappers/paginationOptions.test.tsx
new file mode 100644
index 0000000..b4da27d
--- /dev/null
+++ b/packages/grid-pro-react/tests/mappers/paginationOptions.test.tsx
@@ -0,0 +1,55 @@
+import { describe, it, expect, vi } from 'vitest';
+import { Data, Pagination } from '../../src/index';
+import {
+ mapPaginationEventProps,
+ mergePaginationEventProps
+} from '../../src/utils/mappers/pagination';
+import { getChildProps, normalizeChildOptions } from '@highcharts/grid-shared-react';
+
+describe('mapPaginationEventProps', () => {
+ it('maps pagination event props onto nested option paths', () => {
+ const onBeforePageChange = vi.fn();
+
+ expect(mapPaginationEventProps({
+ page: 1,
+ pageSize: 2,
+ onBeforePageChange
+ })).toEqual({
+ page: 1,
+ pageSize: 2,
+ events: {
+ beforePageChange: onBeforePageChange
+ }
+ });
+ });
+});
+
+describe('mergePaginationEventProps', () => {
+ it('maps pagination event props from declarative child options', () => {
+ const onBeforePageChange = vi.fn();
+ const children = (
+ <>
+
+
+ >
+ );
+
+ const options = mergePaginationEventProps(
+ normalizeChildOptions(getChildProps(children))
+ );
+
+ expect(options.pagination).toEqual({
+ enabled: true,
+ page: 1,
+ pageSize: 2,
+ position: 'bottom',
+ events: {
+ beforePageChange: onBeforePageChange
+ }
+ });
+ });
+});
diff --git a/packages/grid-pro-react/tests/utils/mapEventsProps.test.ts b/packages/grid-pro-react/tests/utils/mapEventsProps.test.ts
new file mode 100644
index 0000000..1a329fb
--- /dev/null
+++ b/packages/grid-pro-react/tests/utils/mapEventsProps.test.ts
@@ -0,0 +1,45 @@
+import { describe, it, expect, vi } from 'vitest';
+import { mapEventsProps } from '../../src/utils/mapEventsProps';
+
+describe('mapEventsProps', () => {
+ it('maps flat event props onto nested option paths', () => {
+ const onCellClick = vi.fn();
+ const column: Record = {
+ id: 'name',
+ onCellClick
+ };
+
+ mapEventsProps(column, {
+ onCellClick: ['cells', 'events', 'click']
+ });
+
+ expect(column).toEqual({
+ id: 'name',
+ cells: {
+ events: {
+ click: onCellClick
+ }
+ }
+ });
+ });
+
+ it('copies handlers from a source object onto nested target paths', () => {
+ const onBeforePageChange = vi.fn();
+ const pagination: Record = { enabled: true };
+
+ mapEventsProps(
+ pagination,
+ {
+ onBeforePageChange: ['events', 'beforePageChange']
+ },
+ { page: 1, onBeforePageChange }
+ );
+
+ expect(pagination).toEqual({
+ enabled: true,
+ events: {
+ beforePageChange: onBeforePageChange
+ }
+ });
+ });
+});
diff --git a/packages/grid-shared-react/src/components/BaseGrid.tsx b/packages/grid-shared-react/src/components/BaseGrid.tsx
index 437459e..b9df08f 100644
--- a/packages/grid-shared-react/src/components/BaseGrid.tsx
+++ b/packages/grid-shared-react/src/components/BaseGrid.tsx
@@ -7,7 +7,7 @@
*
*/
-import { useRef, useImperativeHandle, forwardRef, ForwardedRef } from 'react';
+import { useRef, useImperativeHandle, forwardRef, ForwardedRef, ReactNode } from 'react';
import {
useGrid,
GridType,
@@ -31,7 +31,27 @@ export interface GridProps {
/**
* Grid configuration options
*/
- options: TOptions;
+ options?: TOptions;
+ /**
+ * Optional CSS class names on the React mount container (parent of
+ * `.hcg-container`). Independent of `theme`.
+ */
+ className?: string;
+ /**
+ * Optional CSS class names mapped to Core `rendering.table.className` on
+ * `.hcg-table`. Independent of `className` / `theme`.
+ */
+ tableClassName?: string;
+ /**
+ * Optional theme name passed to Grid Core as `rendering.theme`.
+ * Omitted → Core default (`hcg-theme-default`).
+ * Defined (including `''`) → that value only.
+ */
+ theme?: string;
+ /**
+ * Declarative option components (e.g. Caption) passed as children.
+ */
+ children?: ReactNode;
/**
* Optional ref to access the grid instance
*/
@@ -45,18 +65,18 @@ export interface GridProps {
/**
* Props for BaseGrid component
*/
-export interface BaseGridProps extends GridProps {
- /**
- * Grid instance (from @highcharts/grid-lite or @highcharts/grid-pro)
- */
+export interface BaseGridProps {
+ options?: TOptions;
Grid: GridType;
+ callback?: (grid: GridInstance) => void;
+ className?: string;
}
export const BaseGrid = forwardRef(function BaseGrid(
props: BaseGridProps,
ref: ForwardedRef>
) {
- const { options, Grid, callback } = props;
+ const { options, Grid, callback, className } = props;
const containerRef = useRef(null);
const currGridRef = useGrid({
@@ -76,5 +96,5 @@ export const BaseGrid = forwardRef(function BaseGrid(
[]
);
- return ;
+ return ;
});
diff --git a/packages/grid-shared-react/src/components/BaseGridOptions.ts b/packages/grid-shared-react/src/components/BaseGridOptions.ts
new file mode 100644
index 0000000..aabeb10
--- /dev/null
+++ b/packages/grid-shared-react/src/components/BaseGridOptions.ts
@@ -0,0 +1,34 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+/**
+ * Metadata attached to declarative option components
+ * rendered as BaseGrid children.
+ */
+export interface BaseGridOptions {
+ type: 'Grid_Option';
+ /**
+ * Dot-notation path of the Grid option (e.g. `caption`).
+ */
+ gridOption: string;
+ /**
+ * Sub-option that receives string children (e.g. `text`).
+ */
+ childOption?: string;
+ defaultOptions?: Record;
+ isArrayType?: boolean;
+}
+
+/**
+ * A React component that maps JSX props to a Grid options path
+ * via `_GridReact`.
+ */
+export interface BaseGridOptionsComponent {
+ _GridReact: BaseGridOptions;
+}
diff --git a/packages/grid-shared-react/src/components/options/caption/Caption.tsx b/packages/grid-shared-react/src/components/options/caption/Caption.tsx
new file mode 100644
index 0000000..99c9199
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/caption/Caption.tsx
@@ -0,0 +1,33 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import { ReactNode } from 'react';
+
+export interface CaptionProps {
+ /**
+ * The custom CSS class name for the table caption.
+ */
+ className?: string;
+ /**
+ * The HTML tag to use for the caption.
+ */
+ htmlTag?: string;
+ children?: ReactNode;
+}
+
+export function Caption(_props: CaptionProps) {
+ return null;
+}
+
+Caption._GridReact = {
+ type: 'Grid_Option',
+ gridOption: 'caption',
+ childOption: 'text',
+ isArrayType: false
+};
diff --git a/packages/grid-shared-react/src/components/options/caption/index.ts b/packages/grid-shared-react/src/components/options/caption/index.ts
new file mode 100644
index 0000000..3e96b78
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/caption/index.ts
@@ -0,0 +1,11 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+export { Caption } from './Caption';
+export type { CaptionProps } from './Caption';
diff --git a/packages/grid-shared-react/src/components/options/columns/Column.tsx b/packages/grid-shared-react/src/components/options/columns/Column.tsx
new file mode 100644
index 0000000..f5cd44a
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/columns/Column.tsx
@@ -0,0 +1,20 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import type { ColumnProps } from './columnProps';
+
+export function Column(_props: ColumnProps) {
+ return null;
+}
+
+Column._GridReact = {
+ type: 'Grid_Option',
+ gridOption: 'columns',
+ isArrayType: true
+};
diff --git a/packages/grid-shared-react/src/components/options/columns/ColumnDefaults.tsx b/packages/grid-shared-react/src/components/options/columns/ColumnDefaults.tsx
new file mode 100644
index 0000000..d5a1faf
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/columns/ColumnDefaults.tsx
@@ -0,0 +1,36 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import type { ColumnOptionsProps } from './columnProps';
+
+/**
+ * ColumnDefaults props include shared column options plus grid-level row
+ * class hooks (lifted to `rendering.rows` during normalize).
+ */
+export interface ColumnDefaultsProps extends ColumnOptionsProps {
+ /**
+ * CSS class names on every body `
`.
+ * Maps to Core `rendering.rows.className`.
+ */
+ rowClassName?: string;
+ /**
+ * CSS class names on even body `
` (Core `.hcg-row-even` parity).
+ * Maps to Core `rendering.rows.evenClassName`.
+ */
+ evenRowClassName?: string;
+}
+
+export function ColumnDefaults(_props: ColumnDefaultsProps) {
+ return null;
+}
+
+ColumnDefaults._GridReact = {
+ type: 'Grid_Option',
+ gridOption: 'columnDefaults'
+};
diff --git a/packages/grid-shared-react/src/components/options/columns/columnProps.ts b/packages/grid-shared-react/src/components/options/columns/columnProps.ts
new file mode 100644
index 0000000..1452f99
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/columns/columnProps.ts
@@ -0,0 +1,74 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+export type ColumnDataType = 'string' | 'number' | 'boolean' | 'datetime';
+
+export type ColumnSortingOrder = 'asc' | 'desc' | null;
+
+/**
+ * `this` context passed to `cellValueGetter` by Grid Core.
+ */
+export interface CellValueGetterContext {
+ row: {
+ index: number;
+ };
+}
+
+/**
+ * Shared column options (`columnDefaults` and per-column overrides).
+ */
+export interface ColumnOptionsProps {
+ dataType?: ColumnDataType;
+ width?: number | string;
+ sortingEnabled?: boolean;
+ sortingOrder?: ColumnSortingOrder;
+ sortingPriority?: number;
+ sortingOrderSequence?: ColumnSortingOrder[];
+ sortingCompare?: (a: unknown, b: unknown) => number;
+ filteringEnabled?: boolean;
+ filteringInline?: boolean;
+ filteringCondition?: string;
+ filteringValue?: string | number | boolean | null;
+ headerClassName?: string;
+ headerFormat?: string;
+ headerFormatter?: (this: unknown) => string;
+ headerStyle?: unknown;
+ cellRowHeader?: boolean;
+ cellClassName?: string;
+ cellFormat?: string;
+ cellFormatter?: (this: unknown) => string;
+ /**
+ * Custom cell value resolver. `this` is the Grid table cell (`row.index`
+ * is the row index in the presentation data).
+ */
+ cellValueGetter?: (this: CellValueGetterContext) => unknown;
+ cellContextMenu?: {
+ enabled?: boolean;
+ items?: unknown[];
+ };
+ cellStyle?: unknown;
+ style?: unknown;
+ exportable?: boolean;
+}
+
+export interface ColumnProps extends ColumnOptionsProps {
+ /**
+ * HTML `id` attribute for styling hooks. Not passed to Grid options.
+ */
+ id?: string;
+ /**
+ * References the column to configure (data field id). Maps header, cells,
+ * sorting, filtering, etc. to Grid Core column options.
+ *
+ * Becomes `options.columns[].id` in Grid Core (same identifier).
+ */
+ columnId?: string;
+ className?: string;
+ enabled?: boolean;
+}
diff --git a/packages/grid-shared-react/src/components/options/data/Data.tsx b/packages/grid-shared-react/src/components/options/data/Data.tsx
new file mode 100644
index 0000000..7ec764a
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/data/Data.tsx
@@ -0,0 +1,64 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+export type DataColumnValue = boolean | null | number | string | undefined;
+
+export type DataColumns = Record>;
+
+export interface DataProps {
+ /**
+ * The type of the data provider.
+ *
+ * @default 'local'
+ */
+ providerType?: 'local' | string;
+ /**
+ * Whether columns should be generated automatically from data source
+ * column ids.
+ *
+ * Defaults to `true`. When declarative `` components are used,
+ * the React wrapper sets this to `false` unless you pass this prop
+ * explicitly.
+ *
+ * @default true
+ */
+ autogenerateColumns?: boolean;
+ /**
+ * Columns data to initialize the Grid with.
+ */
+ columns?: DataColumns;
+ /**
+ * Data table as a source of data for the grid.
+ */
+ dataTable?: unknown;
+ /**
+ * Connector instance or options used to populate the data table.
+ */
+ connector?: unknown;
+ /**
+ * Automatically update the grid when the data table changes.
+ *
+ * @default false
+ */
+ updateOnChange?: boolean;
+ /**
+ * The column ID that contains the stable, unique row IDs.
+ */
+ idColumn?: string;
+}
+
+export function Data(_props: DataProps) {
+ return null;
+}
+
+Data._GridReact = {
+ type: 'Grid_Option',
+ gridOption: 'data',
+ isArrayType: false
+};
diff --git a/packages/grid-shared-react/src/components/options/description/Description.tsx b/packages/grid-shared-react/src/components/options/description/Description.tsx
new file mode 100644
index 0000000..c6ee575
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/description/Description.tsx
@@ -0,0 +1,29 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import { ReactNode } from 'react';
+
+export interface DescriptionProps {
+ /**
+ * The custom CSS class name for the description.
+ */
+ className?: string;
+ children?: ReactNode;
+}
+
+export function Description(_props: DescriptionProps) {
+ return null;
+}
+
+Description._GridReact = {
+ type: 'Grid_Option',
+ gridOption: 'description',
+ childOption: 'text',
+ isArrayType: false
+};
diff --git a/packages/grid-shared-react/src/components/options/description/index.ts b/packages/grid-shared-react/src/components/options/description/index.ts
new file mode 100644
index 0000000..65cb487
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/description/index.ts
@@ -0,0 +1,11 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+export { Description } from './Description';
+export type { DescriptionProps } from './Description';
diff --git a/packages/grid-shared-react/src/components/options/header/Header.tsx b/packages/grid-shared-react/src/components/options/header/Header.tsx
new file mode 100644
index 0000000..b952ef3
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/header/Header.tsx
@@ -0,0 +1,19 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import type { HeaderProps } from './headerProps';
+
+export function Header(_props: HeaderProps) {
+ return null;
+}
+
+Header._GridReact = {
+ type: 'Grid_Option',
+ gridOption: 'header'
+};
diff --git a/packages/grid-shared-react/src/components/options/header/headerProps.ts b/packages/grid-shared-react/src/components/options/header/headerProps.ts
new file mode 100644
index 0000000..86f57bb
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/header/headerProps.ts
@@ -0,0 +1,36 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+/**
+ * Accessibility options for a header cell in the header tree.
+ */
+export interface HeaderCellAccessibilityProps {
+ description?: string;
+}
+
+/**
+ * Header node in the `header` tree. A group (with `columns`) or a leaf
+ * (with `columnId`). Mirrors Grid Core `GroupedHeaderOptions`.
+ */
+export interface GroupedHeaderOptions {
+ accessibility?: HeaderCellAccessibilityProps;
+ format?: string;
+ className?: string;
+ columnId?: string;
+ columns?: Array;
+}
+
+export interface HeaderProps {
+ /**
+ * Header tree: column order, inclusion, and grouping.
+ * Each entry is a column id (`string`) or a {@link GroupedHeaderOptions}
+ * object. Maps to Grid Core `options.header`.
+ */
+ header?: Array;
+}
diff --git a/packages/grid-shared-react/src/components/options/header/index.ts b/packages/grid-shared-react/src/components/options/header/index.ts
new file mode 100644
index 0000000..e25312d
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/header/index.ts
@@ -0,0 +1,15 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+export { Header } from './Header';
+export type {
+ HeaderProps,
+ GroupedHeaderOptions,
+ HeaderCellAccessibilityProps
+} from './headerProps';
diff --git a/packages/grid-shared-react/src/components/options/index.ts b/packages/grid-shared-react/src/components/options/index.ts
new file mode 100644
index 0000000..6966a89
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/index.ts
@@ -0,0 +1,33 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+export { Caption } from './caption';
+export type { CaptionProps } from './caption';
+export { Data } from './data/Data';
+export type { DataProps, DataColumns, DataColumnValue } from './data/Data';
+export { ColumnDefaults } from './columns/ColumnDefaults';
+export type { ColumnDefaultsProps } from './columns/ColumnDefaults';
+export { Column } from './columns/Column';
+export type {
+ ColumnProps,
+ ColumnOptionsProps,
+ ColumnDataType,
+ ColumnSortingOrder,
+ CellValueGetterContext
+} from './columns/columnProps';
+export { Description } from './description';
+export type { DescriptionProps } from './description';
+export { Pagination } from './pagination';
+export type { PaginationProps } from './pagination';
+export { Header } from './header';
+export type {
+ HeaderProps,
+ GroupedHeaderOptions,
+ HeaderCellAccessibilityProps
+} from './header';
diff --git a/packages/grid-shared-react/src/components/options/pagination/Pagination.tsx b/packages/grid-shared-react/src/components/options/pagination/Pagination.tsx
new file mode 100644
index 0000000..8a169d7
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/pagination/Pagination.tsx
@@ -0,0 +1,19 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import type { PaginationProps } from './paginationProps';
+
+export function Pagination(_props: PaginationProps) {
+ return null;
+}
+
+Pagination._GridReact = {
+ type: 'Grid_Option',
+ gridOption: 'pagination'
+};
diff --git a/packages/grid-shared-react/src/components/options/pagination/index.ts b/packages/grid-shared-react/src/components/options/pagination/index.ts
new file mode 100644
index 0000000..d5d79cb
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/pagination/index.ts
@@ -0,0 +1,11 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+export { Pagination } from './Pagination';
+export type { PaginationProps } from './paginationProps';
diff --git a/packages/grid-shared-react/src/components/options/pagination/paginationProps.ts b/packages/grid-shared-react/src/components/options/pagination/paginationProps.ts
new file mode 100644
index 0000000..3e31579
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/pagination/paginationProps.ts
@@ -0,0 +1,77 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+export interface PaginationProps {
+ /**
+ * Whether pagination should be rendered.
+ * Defaults to `true` when the `` component is used.
+ * Pass `false` to disable pagination while keeping other options.
+ */
+ enabled?: boolean;
+ /**
+ * Additional CSS class name(s) for the pagination container
+ * (`.hcg-pagination`).
+ */
+ className?: string;
+ /**
+ * Additional CSS class name(s) for the page info element
+ * (`.hcg-pagination-info`).
+ */
+ infoClassName?: string;
+ /**
+ * Additional CSS class name(s) for the controls container
+ * (`.hcg-pagination-controls`).
+ */
+ controlsClassName?: string;
+ /**
+ * Additional CSS class name(s) for the page size container
+ * (`.hcg-pagination-page-size`).
+ */
+ sizeClassName?: string;
+ /**
+ * The current page number.
+ */
+ page?: number;
+ /**
+ * Number of rows per page.
+ */
+ pageSize?: number;
+ /**
+ * Alignment of pagination elements within the wrapper.
+ */
+ align?: 'left' | 'center' | 'right' | 'distributed';
+ /**
+ * Whether to show the page information text.
+ */
+ pageInfo?: boolean;
+ /**
+ * Whether to show the page size selector.
+ */
+ pageSizeSelector?: boolean;
+ /**
+ * Available options for the page size selector dropdown.
+ */
+ pageSizeOptions?: number[];
+ /**
+ * Whether to show numbered page buttons.
+ */
+ pageButtons?: boolean;
+ /**
+ * Maximum number of page number buttons to show before using ellipsis.
+ */
+ pageButtonsCount?: number;
+ /**
+ * Whether to show the first and last page navigation buttons.
+ */
+ firstLast?: boolean;
+ /**
+ * Whether to show the previous and next page navigation buttons.
+ */
+ previousNext?: boolean;
+}
diff --git a/packages/grid-shared-react/src/hooks/useDeclarativeGridOptions.ts b/packages/grid-shared-react/src/hooks/useDeclarativeGridOptions.ts
new file mode 100644
index 0000000..be657fe
--- /dev/null
+++ b/packages/grid-shared-react/src/hooks/useDeclarativeGridOptions.ts
@@ -0,0 +1,60 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import { useMemo, type ReactNode } from 'react';
+import { getChildProps } from '../utils/getChildProps';
+
+export interface OptionsBuildFn {
+ (childOptions: Record, options?: TOptions): TOptions;
+}
+
+export type DeclarativeGridOptionsState = {
+ gridOptions: TOptions;
+ columnKey: string;
+};
+
+/**
+ * Builds a React key that remounts the grid when declarative column ids change.
+ */
+function getColumnKey(childOptions: Record): string {
+ const columns = childOptions.columns as Array<{ id?: string }> | undefined;
+
+ return columns?.map((column) => column.id).join('\0') ?? '';
+}
+
+export interface UseDeclarativeGridOptionsFn {
+ (
+ children: ReactNode | undefined,
+ options: T | undefined,
+ build: OptionsBuildFn,
+ buildDeps?: unknown[]
+ ): DeclarativeGridOptionsState;
+}
+
+export const useDeclarativeGridOptions: UseDeclarativeGridOptionsFn = (
+ children,
+ options,
+ build,
+ buildDeps = []
+) => {
+ const childOptions = useMemo(
+ () => (children != null ? getChildProps(children) : {}),
+ [children]
+ );
+ const columnKey = useMemo(
+ () => getColumnKey(childOptions),
+ [childOptions]
+ );
+ const gridOptions = useMemo(
+ () => build(childOptions, options),
+ [childOptions, options, ...buildDeps]
+ );
+
+ return { gridOptions, columnKey };
+};
diff --git a/packages/grid-shared-react/src/hooks/useGrid.test.tsx b/packages/grid-shared-react/src/hooks/useGrid.test.tsx
index 774a6a1..53e86d1 100644
--- a/packages/grid-shared-react/src/hooks/useGrid.test.tsx
+++ b/packages/grid-shared-react/src/hooks/useGrid.test.tsx
@@ -60,9 +60,10 @@ describe('useGrid', () => {
expect(initQueue).toHaveLength(1);
});
- const [firstInit] = initQueue;
+ const firstInit = initQueue[0];
- await firstInit.resolve();
+ expect(firstInit).toBeDefined();
+ await firstInit!.resolve();
await waitFor(() => {
expect(container.querySelector('[data-grid-id="1"]')).not.toBeNull();
diff --git a/packages/grid-shared-react/src/hooks/useGrid.ts b/packages/grid-shared-react/src/hooks/useGrid.ts
index 327e3fb..f5e1756 100644
--- a/packages/grid-shared-react/src/hooks/useGrid.ts
+++ b/packages/grid-shared-react/src/hooks/useGrid.ts
@@ -8,14 +8,13 @@
*/
import { useEffect, RefObject, useRef } from 'react';
-import { BaseGridProps } from '../components/BaseGrid';
/**
* Interface describing the shape of a Grid instance returned by Grid.grid()
*/
export interface GridInstance {
destroy(): void;
- update(options: TOptions, redraw?: boolean): void;
+ update(options: TOptions, redraw?: boolean, oneToOne?: boolean): void;
}
/**
@@ -25,11 +24,18 @@ export interface GridInstance {
* directly depending on their types.
*/
export interface GridType {
- grid(container: HTMLDivElement, options: TOptions, async?: boolean): GridInstance | Promise>;
+ grid(
+ container: HTMLDivElement,
+ options?: TOptions,
+ async?: boolean
+ ): GridInstance | Promise>;
}
-export interface UseGridOptions extends BaseGridProps {
+export interface UseGridOptions {
containerRef: RefObject;
+ options?: TOptions;
+ Grid: GridType;
+ callback?: (grid: GridInstance) => void;
}
export function useGrid({
@@ -40,7 +46,7 @@ export function useGrid({
}: UseGridOptions) {
const currGridRef = useRef | null>(null);
const callbackRef = useRef(callback);
- const pendingOptionsRef = useRef(null);
+ const pendingOptionsRef = useRef(void 0);
const initStartedRef = useRef(false);
// StrictMode runs effects twice: mount → cleanup → mount.
@@ -60,7 +66,8 @@ export function useGrid({
return;
}
- // StrictMode cleanup runs before re-mount; allow init to complete if re-mounted.
+ // StrictMode cleanup runs before re-mount;
+ // allow init to complete if re-mounted.
destroyOnInitRef.current = false;
// Prevent double initialization
@@ -71,24 +78,27 @@ export function useGrid({
const initGrid = async () => {
try {
- // Use pending options if available (from rapid updates during init)
+ // Use pending options if available
+ // (from rapid updates during init)
const initOptions = pendingOptionsRef.current ?? options;
- pendingOptionsRef.current = null;
+ pendingOptionsRef.current = void 0;
const grid = await Grid.grid(container, initOptions, true);
if (destroyOnInitRef.current) {
- // Component unmounted while we were initializing - destroy immediately
+ // Component unmounted while initializing -
+ // destroy immediately
grid.destroy();
return;
}
currGridRef.current = grid;
- // Apply any pending options that came in while we were initializing
- if (pendingOptionsRef.current) {
- grid.update(pendingOptionsRef.current, true);
- pendingOptionsRef.current = null;
+ // Apply pending options that came in
+ // while we were initializing
+ if (pendingOptionsRef.current !== void 0) {
+ grid.update(pendingOptionsRef.current, true, true);
+ pendingOptionsRef.current = void 0;
}
callbackRef.current?.(grid);
@@ -115,9 +125,14 @@ export function useGrid({
// Effect for options updates - separate from init
useEffect(() => {
+ if (options === void 0) {
+ return;
+ }
+
if (currGridRef.current) {
- // Grid exists, update it directly
- currGridRef.current.update(options, true);
+ // Declarative React options replace the previous
+ // snapshot (oneToOne).
+ currGridRef.current.update(options, true, true);
} else {
// Grid still initializing, queue the update
pendingOptionsRef.current = options;
diff --git a/packages/grid-shared-react/src/index.ts b/packages/grid-shared-react/src/index.ts
index 6a47b1b..23f0e41 100644
--- a/packages/grid-shared-react/src/index.ts
+++ b/packages/grid-shared-react/src/index.ts
@@ -12,4 +12,35 @@ import { GridType, GridInstance } from './hooks/useGrid';
import type { GridProps, GridRefHandle } from './components/BaseGrid';
export { BaseGrid };
+export {
+ Caption,
+ Data,
+ ColumnDefaults,
+ Column,
+ Description,
+ Pagination,
+ Header
+} from './components/options';
+export { getChildProps } from './utils/getChildProps';
+export { isObject } from './utils/isObject';
+export { mergeClassNames } from './utils/mergeClassNames';
+export { normalizeChildOptions } from './utils/normalizeChildOptions';
+export { useDeclarativeGridOptions } from './hooks/useDeclarativeGridOptions';
+export type {
+ CaptionProps,
+ DescriptionProps,
+ DataProps,
+ DataColumns,
+ DataColumnValue,
+ ColumnProps,
+ ColumnOptionsProps,
+ ColumnDefaultsProps,
+ ColumnDataType,
+ ColumnSortingOrder,
+ CellValueGetterContext,
+ PaginationProps,
+ HeaderProps,
+ GroupedHeaderOptions,
+ HeaderCellAccessibilityProps
+} from './components/options';
export type { GridType, GridInstance, GridProps, GridRefHandle };
diff --git a/packages/grid-shared-react/src/utils/getChildProps.ts b/packages/grid-shared-react/src/utils/getChildProps.ts
new file mode 100644
index 0000000..aca622a
--- /dev/null
+++ b/packages/grid-shared-react/src/utils/getChildProps.ts
@@ -0,0 +1,343 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import { Fragment, isValidElement, ReactElement, ReactNode } from 'react';
+import type { BaseGridOptionsComponent, BaseGridOptions } from '../components/BaseGridOptions';
+import { isObject } from './isObject';
+
+function flattenChildren(childNodes: ReactNode): ReactNode[] {
+ if (childNodes == null || childNodes === false) {
+ return [];
+ }
+
+ if (Array.isArray(childNodes)) {
+ return childNodes.flatMap((child) => flattenChildren(child));
+ }
+
+ if (isValidElement(childNodes) && childNodes.type === Fragment) {
+ const fragmentProps = childNodes.props as { children?: ReactNode };
+ return flattenChildren(fragmentProps.children);
+ }
+
+ return [childNodes];
+}
+
+function objInsert(
+ obj: Record,
+ path: string,
+ value: unknown
+): Record {
+ const keys = path.split('.');
+ let current = obj;
+
+ for (let i = 0; i < keys.length - 1; i++) {
+ const key = keys[i];
+
+ if (key === void 0) {
+ continue;
+ }
+
+ if (!isObject(current[key])) {
+ current[key] = {};
+ }
+ current = current[key] as Record;
+ }
+
+ const lastKey = keys.at(-1);
+
+ if (lastKey !== void 0) {
+ current[lastKey] = value;
+ }
+ return obj;
+}
+
+function isReactElement(value: unknown): value is ReactElement {
+ return isValidElement(value);
+}
+
+function getOptionComponent(type: unknown): BaseGridOptionsComponent | null {
+ if (typeof type !== 'function' && (typeof type !== 'object' || type === null)) {
+ return null;
+ }
+
+ const component = type as Partial;
+
+ return component._GridReact ? type as BaseGridOptionsComponent : null;
+}
+
+function getChildPropsFromElement(
+ child: ReactElement
+): Record {
+ return (child.props ?? {}) as Record;
+}
+
+function renderChildren(children: ReactNode): string {
+ if (typeof children === 'string' || typeof children === 'number') {
+ return String(children);
+ }
+
+ if (Array.isArray(children)) {
+ return children
+ .map((child) => renderChildren(child))
+ .join('');
+ }
+
+ return '';
+}
+
+function getEffectiveMeta(
+ component: BaseGridOptionsComponent,
+ parentMeta?: BaseGridOptions
+): BaseGridOptions {
+ const meta = component._GridReact;
+
+ if (!parentMeta) {
+ return meta;
+ }
+
+ return {
+ ...meta,
+ childOption: parentMeta.childOption
+ ? `${parentMeta.childOption}.${meta.childOption ?? ''}`
+ : meta.childOption,
+ gridOption: parentMeta.gridOption
+ ? `${parentMeta.gridOption}.${meta.gridOption}`
+ : meta.gridOption
+ };
+}
+
+function parseColumnElement(child: ReactElement): Record {
+ const {
+ children,
+ id,
+ columnId,
+ ...props
+ } = getChildPropsFromElement(child);
+ void children;
+ void id;
+
+ // columnId selects the column; Core expects the same value as `id`.
+ if (columnId !== void 0) {
+ props.id = columnId;
+ }
+
+ return props;
+}
+
+function pushColumn(
+ optionsFromChildren: Record,
+ child: ReactElement
+): void {
+ const columns = (optionsFromChildren.columns ?? (
+ optionsFromChildren.columns = []
+ )) as Record[];
+
+ columns.push(parseColumnElement(child));
+}
+
+export function getChildProps(children: ReactNode): Record {
+ const optionsFromChildren: Record = {};
+ const resolvedChildren = flattenChildren(children)
+ .map((child) => resolveOptionChild(child))
+ .filter((child): child is ReactElement => child !== null);
+
+ function handleChildren(
+ childNodes: ReactNode,
+ obj: Record,
+ meta: BaseGridOptions
+ ): void {
+ if (childNodes == null || childNodes === false) {
+ return;
+ }
+
+ const nonOptionChildren: ReactNode[] = [];
+
+ if (Array.isArray(childNodes)) {
+ for (const child of childNodes) {
+ if (isReactElement(child) && isOptionElement(child)) {
+ handleChild(child, meta);
+ continue;
+ }
+
+ nonOptionChildren.push(child);
+ }
+ } else if (isReactElement(childNodes) && isOptionElement(childNodes)) {
+ handleChild(childNodes, meta);
+ } else {
+ nonOptionChildren.push(childNodes);
+ }
+
+ if (meta.childOption) {
+ const childrenToRender = nonOptionChildren.length > 0 ?
+ nonOptionChildren :
+ [childNodes];
+
+ objInsert(obj, meta.childOption, renderChildren(childrenToRender));
+ }
+ }
+
+ function handleChild(
+ child: ReactElement,
+ parentMeta?: BaseGridOptions
+ ): void {
+ const component = getOptionComponent(child.type);
+
+ if (!component) {
+ return;
+ }
+
+ const meta = getEffectiveMeta(component, parentMeta);
+
+ if (!meta.gridOption) {
+ return;
+ }
+
+ const childProps = getChildPropsFromElement(child);
+ const { children: childChildren, ...props } = childProps;
+
+ if (meta.gridOption === 'columnDefaults') {
+ optionsFromChildren.columnDefaults = props;
+ return;
+ }
+
+ if (meta.gridOption === 'columns') {
+ pushColumn(optionsFromChildren, child);
+ return;
+ }
+
+ if (meta.gridOption === 'pagination') {
+ optionsFromChildren.pagination = {
+ ...props,
+ position: isTopPaginationChild(
+ child,
+ resolvedChildren
+ ) ? 'top' : 'bottom'
+ };
+ return;
+ }
+
+ if (meta.gridOption === 'header') {
+ if (props.header !== void 0) {
+ optionsFromChildren.header = props.header;
+ }
+ return;
+ }
+
+ const optionParent = optionsFromChildren[meta.gridOption] ?? (
+ optionsFromChildren[meta.gridOption] = meta.isArrayType ? [] : {}
+ );
+ const parentIsArray = Array.isArray(optionParent);
+ const insertInto = parentIsArray
+ ? {}
+ : optionParent as Record;
+
+ if (meta.defaultOptions) {
+ Object.assign(insertInto, meta.defaultOptions);
+ }
+
+ Object.assign(insertInto, props);
+
+ if (typeof childChildren === 'string' || typeof childChildren === 'number') {
+ if (meta.childOption) {
+ objInsert(insertInto, meta.childOption, String(childChildren));
+ }
+ } else if (childChildren != null) {
+ handleChildren(childChildren as ReactNode, insertInto, meta);
+ }
+
+ if (parentIsArray) {
+ const optionItems = optionsFromChildren[
+ meta.gridOption
+ ] as unknown[];
+ optionItems.push(insertInto);
+ }
+ }
+
+ for (const child of resolvedChildren) {
+ handleChild(child);
+ }
+
+ applyDeclarativeColumnDefaults(optionsFromChildren);
+
+ return optionsFromChildren;
+}
+
+/**
+ * When declarative `` components are present, only those columns
+ * should render unless `data.autogenerateColumns` is set
+ * explicitly on ``.
+ */
+function applyDeclarativeColumnDefaults(
+ optionsFromChildren: Record
+): void {
+ const columns = optionsFromChildren.columns;
+
+ if (!Array.isArray(columns) || columns.length === 0) {
+ return;
+ }
+
+ const data = isObject(optionsFromChildren.data) ?
+ { ...optionsFromChildren.data } :
+ {};
+
+ if (!('autogenerateColumns' in data)) {
+ data.autogenerateColumns = false;
+ optionsFromChildren.data = data;
+ }
+}
+
+function isTopPaginationChild(
+ child: ReactElement,
+ children: ReactElement[]
+): boolean {
+ const childIndex = children.indexOf(child);
+
+ if (childIndex === -1) {
+ return false;
+ }
+
+ return children
+ .slice(0, childIndex)
+ .every((candidate) => {
+ const gridOption = getOptionComponent(candidate.type)
+ ?._GridReact.gridOption;
+ return gridOption === 'pagination';
+ });
+}
+
+function isOptionElement(child: ReactElement): boolean {
+ return getOptionComponent(child.type) !== null;
+}
+
+function resolveOptionChild(child: ReactNode): ReactElement | null {
+ if (!isReactElement(child)) {
+ return null;
+ }
+
+ const component = getOptionComponent(child.type);
+
+ if (component) {
+ return child;
+ }
+
+ if (typeof child.type !== 'function') {
+ return null;
+ }
+
+ const renderChild = child.type as (
+ props: Record
+ ) => ReactNode;
+ const rendered = renderChild(getChildPropsFromElement(child));
+
+ if (isReactElement(rendered) && getOptionComponent(rendered.type)) {
+ return rendered;
+ }
+
+ return null;
+}
diff --git a/packages/grid-shared-react/src/utils/isObject.ts b/packages/grid-shared-react/src/utils/isObject.ts
new file mode 100644
index 0000000..636cdf0
--- /dev/null
+++ b/packages/grid-shared-react/src/utils/isObject.ts
@@ -0,0 +1,14 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+export function isObject(
+ value: unknown
+): value is Record {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
diff --git a/packages/grid-shared-react/src/utils/mappers/column/columnOptions.ts b/packages/grid-shared-react/src/utils/mappers/column/columnOptions.ts
new file mode 100644
index 0000000..f1ae833
--- /dev/null
+++ b/packages/grid-shared-react/src/utils/mappers/column/columnOptions.ts
@@ -0,0 +1,24 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import { mapPrefixedProps } from '../mapPrefixedProps';
+
+/** Flat prop prefix → nested Grid option key for columns. */
+const COLUMN_PROP_PREFIXES = {
+ sorting: 'sorting',
+ filtering: 'filtering',
+ header: 'header',
+ cell: 'cells'
+} as const;
+
+export function normalizeColumnOptions(
+ props: Record
+): Record {
+ return mapPrefixedProps(props, COLUMN_PROP_PREFIXES);
+}
diff --git a/packages/grid-shared-react/src/utils/mappers/column/index.ts b/packages/grid-shared-react/src/utils/mappers/column/index.ts
new file mode 100644
index 0000000..01a620a
--- /dev/null
+++ b/packages/grid-shared-react/src/utils/mappers/column/index.ts
@@ -0,0 +1,10 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+export { normalizeColumnOptions } from './columnOptions';
diff --git a/packages/grid-shared-react/src/utils/mappers/mapPrefixedProps.ts b/packages/grid-shared-react/src/utils/mappers/mapPrefixedProps.ts
new file mode 100644
index 0000000..82418e3
--- /dev/null
+++ b/packages/grid-shared-react/src/utils/mappers/mapPrefixedProps.ts
@@ -0,0 +1,61 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+/**
+ * Maps flat props with a shared prefix into nested Grid option objects.
+ *
+ * Convention: `{prefix}{OptionKey}` → `{groupKey}.{optionKey}`
+ *
+ * @example
+ * mapPrefixedProps(
+ * { sortingEnabled: true, sortingOrder: 'asc', width: 120 },
+ * { sorting: 'sorting' }
+ * );
+ * // => { width: 120, sorting: { enabled: true, order: 'asc' } }
+ */
+export type PrefixedPropMap = Record;
+
+export function mapPrefixedProps(
+ props: Record,
+ prefixToGroup: PrefixedPropMap
+): Record {
+ const result = { ...props };
+ const groups: Record> = {};
+ const prefixes = Object.keys(prefixToGroup)
+ .sort((a, b) => b.length - a.length);
+
+ for (const flatKey of Object.keys(result)) {
+ const prefix = prefixes.find(
+ (candidate) => (
+ flatKey.startsWith(candidate)
+ && flatKey.length > candidate.length
+ )
+ );
+
+ if (!prefix) {
+ continue;
+ }
+
+ const groupKey = prefixToGroup[prefix];
+ const nestedKey = toNestedKey(flatKey.slice(prefix.length));
+
+ (groups[groupKey] ??= {})[nestedKey] = result[flatKey];
+ delete result[flatKey];
+ }
+
+ for (const [groupKey, nested] of Object.entries(groups)) {
+ result[groupKey] = nested;
+ }
+
+ return result;
+}
+
+function toNestedKey(segment: string): string {
+ return segment.charAt(0).toLowerCase() + segment.slice(1);
+}
diff --git a/packages/grid-shared-react/src/utils/mappers/pagination/index.ts b/packages/grid-shared-react/src/utils/mappers/pagination/index.ts
new file mode 100644
index 0000000..de3e9f4
--- /dev/null
+++ b/packages/grid-shared-react/src/utils/mappers/pagination/index.ts
@@ -0,0 +1,10 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+export { normalizePaginationOptions } from './paginationOptions';
diff --git a/packages/grid-shared-react/src/utils/mappers/pagination/paginationOptions.ts b/packages/grid-shared-react/src/utils/mappers/pagination/paginationOptions.ts
new file mode 100644
index 0000000..6730758
--- /dev/null
+++ b/packages/grid-shared-react/src/utils/mappers/pagination/paginationOptions.ts
@@ -0,0 +1,133 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+function withClassName(
+ value: unknown,
+ className: string | undefined
+): unknown {
+ if (className === void 0) {
+ return value;
+ }
+
+ // Disabled control is not rendered — keep boolean, drop className.
+ if (value === false) {
+ return false;
+ }
+
+ if (value !== null && typeof value === 'object') {
+ return { ...value as Record, className };
+ }
+
+ return {
+ enabled: value === void 0 ? true : Boolean(value),
+ className
+ };
+}
+
+export function normalizePaginationOptions(
+ props: Record
+): Record {
+ const {
+ pageInfo,
+ pageSizeSelector,
+ pageSizeOptions,
+ pageButtons,
+ pageButtonsCount,
+ firstLast,
+ previousNext,
+ enabled,
+ page,
+ pageSize,
+ align,
+ className,
+ infoClassName,
+ controlsClassName,
+ sizeClassName,
+ ...rest
+ } = props;
+
+ const result: Record = {
+ enabled: enabled ?? true
+ };
+
+ if (page !== void 0) {
+ result.page = page;
+ }
+ if (pageSize !== void 0) {
+ result.pageSize = pageSize;
+ }
+ if (align !== void 0) {
+ result.align = align;
+ }
+ if (typeof className === 'string') {
+ result.className = className;
+ }
+
+ const controls: Record = {};
+
+ if (typeof controlsClassName === 'string') {
+ controls.className = controlsClassName;
+ }
+
+ const pageInfoValue = withClassName(pageInfo, asString(infoClassName));
+ if (pageInfoValue !== void 0) {
+ controls.pageInfo = pageInfoValue;
+ }
+
+ let pageSizeSelectorValue: unknown = pageSizeSelector;
+
+ if (pageSizeSelector === false) {
+ pageSizeSelectorValue = false;
+ } else if (pageSizeOptions !== void 0) {
+ pageSizeSelectorValue = {
+ enabled: true,
+ options: pageSizeOptions
+ };
+ } else if (pageSizeSelector !== void 0) {
+ pageSizeSelectorValue = pageSizeSelector;
+ }
+
+ pageSizeSelectorValue = withClassName(
+ pageSizeSelectorValue,
+ asString(sizeClassName)
+ );
+
+ if (pageSizeSelectorValue !== void 0) {
+ controls.pageSizeSelector = pageSizeSelectorValue;
+ }
+
+ if (pageButtons === false) {
+ controls.pageButtons = false;
+ } else if (pageButtonsCount !== void 0) {
+ controls.pageButtons = {
+ enabled: true,
+ count: pageButtonsCount
+ };
+ } else if (pageButtons !== void 0) {
+ controls.pageButtons = pageButtons;
+ }
+
+ if (firstLast !== void 0) {
+ controls.firstLastButtons = firstLast;
+ }
+
+ if (previousNext !== void 0) {
+ controls.previousNextButtons = previousNext;
+ }
+
+ if (Object.keys(controls).length > 0) {
+ result.controls = controls;
+ }
+
+ return { ...result, ...rest };
+}
+
+function asString(value: unknown): string | undefined {
+ return typeof value === 'string' ? value : void 0;
+}
diff --git a/packages/grid-shared-react/src/utils/mergeClassNames.ts b/packages/grid-shared-react/src/utils/mergeClassNames.ts
new file mode 100644
index 0000000..d4f6737
--- /dev/null
+++ b/packages/grid-shared-react/src/utils/mergeClassNames.ts
@@ -0,0 +1,32 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+/**
+ * Joins CSS class tokens from defaults and overrides without duplicates.
+ * Used so ColumnDefaults `*ClassName` still applies when a Column sets its
+ * own `*ClassName`.
+ */
+export function mergeClassNames(
+ ...classNames: Array<(string | undefined | null)>
+): string | undefined {
+ const tokens: string[] = [];
+
+ for (const value of classNames) {
+ if (typeof value !== 'string' || !value.trim()) {
+ continue;
+ }
+ for (const token of value.trim().split(/\s+/)) {
+ if (token && !tokens.includes(token)) {
+ tokens.push(token);
+ }
+ }
+ }
+
+ return tokens.length ? tokens.join(' ') : void 0;
+}
diff --git a/packages/grid-shared-react/src/utils/normalizeChildOptions.ts b/packages/grid-shared-react/src/utils/normalizeChildOptions.ts
new file mode 100644
index 0000000..5eb09b7
--- /dev/null
+++ b/packages/grid-shared-react/src/utils/normalizeChildOptions.ts
@@ -0,0 +1,142 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import { isObject } from './isObject';
+import { mergeClassNames } from './mergeClassNames';
+import { normalizeColumnOptions } from './mappers/column';
+import { normalizePaginationOptions } from './mappers/pagination';
+
+/**
+ * Maps raw declarative child options onto nested Grid option paths.
+ * Used by lite and pro build pipelines after `getChildProps`.
+ *
+ * Also merges `columnDefaults` `className` / `header.className` /
+ * `cells.className` into each column so Column overrides do not wipe
+ * defaults (same semantics as Core `createOptionsProxy`).
+ *
+ * Lifts `rowClassName` / `evenRowClassName` from ColumnDefaults onto
+ * `rendering.rows` (not column options).
+ */
+export function normalizeChildOptions(
+ raw: Record
+): Record {
+ const result = { ...raw };
+
+ if (isObject(result.columnDefaults)) {
+ const defaults = { ...result.columnDefaults };
+ const rowClassName = defaults.rowClassName;
+ const evenRowClassName = defaults.evenRowClassName;
+ delete defaults.rowClassName;
+ delete defaults.evenRowClassName;
+
+ result.columnDefaults = normalizeColumnOptions(defaults);
+
+ const rows: Record = {};
+ if (typeof rowClassName === 'string') {
+ rows.className = rowClassName;
+ }
+ if (typeof evenRowClassName === 'string') {
+ rows.evenClassName = evenRowClassName;
+ }
+
+ if (Object.keys(rows).length) {
+ const rendering = isObject(result.rendering) ?
+ { ...result.rendering } :
+ {};
+ const existingRows = isObject(rendering.rows) ?
+ { ...rendering.rows as Record } :
+ {};
+ rendering.rows = { ...existingRows, ...rows };
+ result.rendering = rendering;
+ }
+ }
+
+ if (Array.isArray(result.columns)) {
+ const defaults = isObject(result.columnDefaults) ?
+ result.columnDefaults as Record :
+ void 0;
+
+ result.columns = result.columns.map((column) => {
+ if (!isObject(column)) {
+ return column;
+ }
+
+ const normalized = normalizeColumnOptions({ ...column });
+ return defaults ?
+ mergeColumnClassNames(normalized, defaults) :
+ normalized;
+ });
+ }
+
+ if (isObject(result.pagination)) {
+ const pagination = { ...result.pagination };
+ const { position, ...props } = pagination;
+ const normalized = normalizePaginationOptions(props);
+
+ if (position !== void 0) {
+ normalized.position = position;
+ }
+
+ result.pagination = normalized;
+ }
+
+ return result;
+}
+
+function mergeColumnClassNames(
+ column: Record,
+ defaults: Record
+): Record {
+ const result = { ...column };
+ const className = mergeClassNames(
+ asString(defaults.className),
+ asString(column.className)
+ );
+
+ if (className !== void 0) {
+ result.className = className;
+ }
+
+ result.header = mergeNestedClassName(
+ column.header,
+ defaults.header
+ );
+ result.cells = mergeNestedClassName(
+ column.cells,
+ defaults.cells
+ );
+
+ return result;
+}
+
+function mergeNestedClassName(
+ target: unknown,
+ defaults: unknown
+): Record | unknown {
+ const targetObj = isObject(target) ? { ...target } : {};
+ const defaultsObj = isObject(defaults) ? defaults : {};
+ const className = mergeClassNames(
+ asString(defaultsObj.className),
+ asString(targetObj.className)
+ );
+
+ if (className === void 0 && !isObject(target)) {
+ return target;
+ }
+
+ if (className !== void 0) {
+ targetObj.className = className;
+ }
+
+ return targetObj;
+}
+
+function asString(value: unknown): string | undefined {
+ return typeof value === 'string' ? value : void 0;
+}
diff --git a/packages/grid-shared-react/src/test/createGridTests.tsx b/packages/grid-shared-react/tests/createGridTests.tsx
similarity index 73%
rename from packages/grid-shared-react/src/test/createGridTests.tsx
rename to packages/grid-shared-react/tests/createGridTests.tsx
index 1faed12..29a1c7d 100644
--- a/packages/grid-shared-react/src/test/createGridTests.tsx
+++ b/packages/grid-shared-react/tests/createGridTests.tsx
@@ -5,18 +5,23 @@ import {
type ComponentType
} from 'react';
import { describe, it, expect, vi } from 'vitest';
-import { GridProps, GridRefHandle } from '../components/BaseGrid';
-import { GridInstance } from '../hooks/useGrid';
+import { GridProps, GridRefHandle } from '../src/components/BaseGrid';
+import { GridInstance } from '../src/hooks/useGrid';
/**
* Creates a standard test suite for a Grid component.
- * Use this to avoid duplicating tests between grid-lite-react and grid-pro-react.
+ * Use this to avoid duplicating tests between
+ * grid-lite-react and grid-pro-react.
*/
-export function createGridTests(
+export function createGridTests<
+ TOptions,
+ TComponentProps extends Record = Record
+>(
name: string,
- GridComponent: ComponentType>,
+ GridComponent: ComponentType & TComponentProps>,
testOptions: TOptions,
- updatedOptions: TOptions
+ updatedOptions: TOptions,
+ componentProps?: TComponentProps
) {
describe(name, () => {
@@ -28,7 +33,11 @@ export function createGridTests(
};
const { container } = render(
-
+
);
expect(container.firstChild).toBeInstanceOf(HTMLDivElement);
@@ -46,6 +55,7 @@ export function createGridTests(
gridRef = useRef>(null);
return (
{ initialized = true; }}
@@ -63,7 +73,13 @@ export function createGridTests(
it('calls callback when grid is initialized', async () => {
const callback = vi.fn();
- render();
+ render(
+
+ );
await waitFor(() => {
expect(callback).toHaveBeenCalled();
@@ -82,7 +98,11 @@ export function createGridTests(
return (
<>
-
+