diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index c844038..3cceffc 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,9 +1,31 @@
-name: Tests
+name: CI
on:
pull_request:
jobs:
+ lint:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'pnpm'
+
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Run linter
+ run: pnpm lint
+
test:
runs-on: ubuntu-latest
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100755
index 0000000..acd310a
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,2 @@
+pnpm lint
+pnpm test
diff --git a/eslint.config.js b/eslint.config.js
index a14c2bd..5e17dfa 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -6,7 +6,12 @@ import globals from 'globals';
export default defineConfig(
{
- ignores: ['**/dist/**', '**/build/**'],
+ ignores: [
+ '**/dist/**',
+ '**/build/**',
+ '**/.next/**',
+ '**/node_modules/**'
+ ],
},
eslint.configs.recommended,
tseslint.configs.recommended,
@@ -17,15 +22,24 @@ export default defineConfig(
},
rules: {
'curly': ['error', 'all'],
+ '@typescript-eslint/no-unused-vars': ['error', {
+ argsIgnorePattern: '^_'
+ }],
'@stylistic/semi': ['error', 'always'],
'@stylistic/quotes': ['error', 'single', { avoidEscape: true }],
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: true }],
'@stylistic/eol-last': ['error', 'always'],
- '@stylistic/no-trailing-spaces': ['error']
+ '@stylistic/no-trailing-spaces': ['error'],
+ '@stylistic/max-len': ['error', {
+ code: 80,
+ ignoreUrls: true,
+ ignoreStrings: true,
+ ignoreTemplateLiterals: true
+ }]
},
},
{
- files: ['scripts/**/*.js'],
+ files: ['scripts/**/*.js', '**/next.config.js'],
languageOptions: {
globals: {
...globals.node,
diff --git a/examples/grid-lite/components-react/index.html b/examples/grid-lite/components-react/index.html
new file mode 100644
index 0000000..7f1e258
--- /dev/null
+++ b/examples/grid-lite/components-react/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ Highcharts Grid Lite - React Example
+
+
+
+
+
+
+
diff --git a/examples/grid-lite/components-react/package.json b/examples/grid-lite/components-react/package.json
new file mode 100644
index 0000000..cd5b45a
--- /dev/null
+++ b/examples/grid-lite/components-react/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "grid-lite-minimal-react",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "clean": "rimraf dist node_modules"
+ },
+ "dependencies": {
+ "@highcharts/grid-lite": ">=3.0.0",
+ "@highcharts/grid-lite-react": "workspace:*",
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "devDependencies": {
+ "@types/react": ">=18",
+ "@types/react-dom": ">=18",
+ "@vitejs/plugin-react": "^4.2.0",
+ "typescript": "^5.0.0",
+ "vite": "^5.0.0"
+ }
+}
+
diff --git a/examples/grid-lite/components-react/src/App.tsx b/examples/grid-lite/components-react/src/App.tsx
new file mode 100644
index 0000000..9a2c3eb
--- /dev/null
+++ b/examples/grid-lite/components-react/src/App.tsx
@@ -0,0 +1,189 @@
+import {
+ useState,
+ // useRef
+} from 'react';
+import {
+ type GridInstance,
+ // type GridRefHandle,
+ type GridOptions,
+ Grid,
+ Caption,
+ Data,
+ // DataTable,
+ ColumnDefaults,
+ Column,
+ Description,
+ Pagination,
+ Header
+} from '@highcharts/grid-lite-react';
+
+function App() {
+ // const grid = useRef | null>(null);
+
+ // ==== OPTIONS ====
+ // const [options] = useState({
+ // dataTable: {
+ // columns: {
+ // name: ['1111Alice', 'Bob', 'Charlie', 'David', 'Eve'],
+ // age: [23, 34, 45, 56, 67],
+ // city: ['New York', 'Oslo', 'Paris', 'Tokyo', 'London'],
+ // salary: [50000, 60000, 70000, 80000, 90000]
+ // }
+ // }
+ // });
+
+ // ==== DATA ====
+ // Data Columns
+ const [dataSource, setDataSource] = useState({
+ name: ['COLUMNS', 'Bob', 'Charlie', 'David', 'Eve'],
+ age: [23, 34, 45, 56, 67],
+ city: ['New York', 'Oslo', 'Paris', 'Tokyo', 'London'],
+ salary: [50000, 60000, 70000, 80000, 90000]
+ });
+
+ // Data Table
+ // const dataTable = new DataTable({
+ // columns: {
+ // name: ['DATATABLE', 'Bob', 'Charlie', 'David', 'Eve'],
+ // age: [23, 34, 45, 56, 67],
+ // city: ['New York', 'Oslo', 'Paris', 'Tokyo', 'London'],
+ // salary: [50000, 60000, 70000, 80000, 90000]
+ // }
+ // });
+
+ // ==== ACTIONS ====
+ const onButtonClick = () => {
+ // console.info('(ref) grid:', grid.current?.grid);
+ setDataSource({
+ name: ['John', 'Jane', 'Jim', 'Jill', 'Jack'],
+ age: [30, 25, 35, 40, 45],
+ city: ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Miami'],
+ salary: [40000, 35000, 45000, 50000, 55000]
+ });
+ };
+
+ const onGridCallback = (grid: GridInstance) => {
+ console.info('(callback) grid:', grid);
+ };
+
+ // Pagination
+ // const [paginationEnabled, setPaginationEnabled] = useState(false);
+
+ // const onPaginationClick = () => {
+ // setPaginationEnabled(true);
+ // };
+
+ return (
+ <>
+
+ {/* */}
+
+
+ Grid Caption v2.1
+
+
+
+
+
+
+ Grid Description
+
+
+
+
+ {/* */}
+
+ >
+ );
+}
+
+export default App;
diff --git a/examples/grid-lite/components-react/src/index.css b/examples/grid-lite/components-react/src/index.css
new file mode 100644
index 0000000..9a63579
--- /dev/null
+++ b/examples/grid-lite/components-react/src/index.css
@@ -0,0 +1,33 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+#root {
+ width: 100%;
+ min-height: 100vh;
+ padding: 20px;
+}
+
+#controls {
+ margin-top: 20px;
+ display: flex;
+ gap: 10px;
+}
+
+@media (prefers-color-scheme: dark) {
+ body {
+ background-color: #121212;
+ color: #ffffff;
+ }
+}
+
diff --git a/examples/grid-lite/components-react/src/main.tsx b/examples/grid-lite/components-react/src/main.tsx
new file mode 100644
index 0000000..0c657b5
--- /dev/null
+++ b/examples/grid-lite/components-react/src/main.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+import './index.css';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
+
diff --git a/examples/grid-lite/components-react/tsconfig.json b/examples/grid-lite/components-react/tsconfig.json
new file mode 100644
index 0000000..78a6daf
--- /dev/null
+++ b/examples/grid-lite/components-react/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "lib": [
+ "DOM",
+ "ES2016",
+ "ES2017.Object"
+ ],
+ "jsx": "react-jsx",
+ "module": "ES6",
+ "moduleResolution": "node",
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitThis": true,
+ "noFallthroughCasesInSwitch": true,
+ "skipDefaultLibCheck": true,
+ "skipLibCheck": true,
+ "ignoreDeprecations": "5.0",
+ "allowSyntheticDefaultImports": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
+
diff --git a/examples/grid-lite/components-react/tsconfig.node.json b/examples/grid-lite/components-react/tsconfig.node.json
new file mode 100644
index 0000000..f7c2070
--- /dev/null
+++ b/examples/grid-lite/components-react/tsconfig.node.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "types": ["node"]
+ },
+ "include": ["vite.config.ts"]
+}
+
diff --git a/examples/grid-lite/components-react/vite.config.ts b/examples/grid-lite/components-react/vite.config.ts
new file mode 100644
index 0000000..2e3f28b
--- /dev/null
+++ b/examples/grid-lite/components-react/vite.config.ts
@@ -0,0 +1,30 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { resolve, dirname } from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: [
+ {
+ find: '@highcharts/grid-lite-react',
+ replacement: resolve(__dirname, '../../../packages/grid-lite-react/src/index.ts')
+ },
+ {
+ find: '@highcharts/grid-shared-react',
+ replacement: resolve(__dirname, '../../../packages/grid-shared-react/src/index.ts')
+ },
+ {
+ find: /^@highcharts\/grid-lite(\/.*)?$/,
+ replacement: resolve(__dirname, 'node_modules/@highcharts/grid-lite$1')
+ }
+ ]
+ },
+ server: {
+ port: 3000
+ }
+});
+
diff --git a/examples/grid-lite/minimal-nextjs/app/page.tsx b/examples/grid-lite/minimal-nextjs/app/page.tsx
index 1d82cc4..5d5de97 100644
--- a/examples/grid-lite/minimal-nextjs/app/page.tsx
+++ b/examples/grid-lite/minimal-nextjs/app/page.tsx
@@ -48,8 +48,12 @@ export default function Home() {
};
return (
- <>
-
+ <>
+
>
);
diff --git a/examples/grid-pro/components-react/index.html b/examples/grid-pro/components-react/index.html
new file mode 100644
index 0000000..b07a6f3
--- /dev/null
+++ b/examples/grid-pro/components-react/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Highcharts Grid Pro - React Example
+
+
+
+
+
+
diff --git a/examples/grid-pro/components-react/package.json b/examples/grid-pro/components-react/package.json
new file mode 100644
index 0000000..e70810f
--- /dev/null
+++ b/examples/grid-pro/components-react/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "grid-pro-components-react",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "clean": "rimraf dist node_modules"
+ },
+ "dependencies": {
+ "@highcharts/grid-pro": ">=3.0.0",
+ "@highcharts/grid-pro-react": "workspace:*",
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "devDependencies": {
+ "@types/react": ">=18",
+ "@types/react-dom": ">=18",
+ "@vitejs/plugin-react": "^4.2.0",
+ "typescript": "^5.0.0",
+ "vite": "^5.0.0"
+ }
+}
diff --git a/examples/grid-pro/components-react/src/App.tsx b/examples/grid-pro/components-react/src/App.tsx
new file mode 100644
index 0000000..2d7f839
--- /dev/null
+++ b/examples/grid-pro/components-react/src/App.tsx
@@ -0,0 +1,98 @@
+import { useState } from 'react';
+import {
+ type GridInstance,
+ type GridOptions,
+ Grid,
+ Caption,
+ Data,
+ ColumnDefaults,
+ Column,
+ Description,
+ Pagination
+} from '@highcharts/grid-pro-react';
+
+const GRID_KEY = 'AAAA-BBBB-CCCC-DDDD-EEEE-FFFF';
+
+function App() {
+ const [dataSource, setDataSource] = useState({
+ name: ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
+ age: [23, 34, 45, 56, 67],
+ city: ['New York', 'Oslo', 'Paris', 'Tokyo', 'London'],
+ salary: [50000, 60000, 70000, 80000, 90000]
+ });
+
+ const onButtonClick = () => {
+ setDataSource({
+ name: ['John', 'Jane', 'Jim', 'Jill', 'Jack'],
+ age: [30, 25, 35, 40, 45],
+ city: ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Miami'],
+ salary: [40000, 35000, 45000, 50000, 55000]
+ });
+ };
+
+ const onGridCallback = (grid: GridInstance) => {
+ console.info('(callback) grid:', grid);
+ };
+
+ return (
+ <>
+
+
+
+ Grid Pro Components
+ Declarative API with gridKey and event props
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default App;
diff --git a/examples/grid-pro/components-react/src/index.css b/examples/grid-pro/components-react/src/index.css
new file mode 100644
index 0000000..16edde1
--- /dev/null
+++ b/examples/grid-pro/components-react/src/index.css
@@ -0,0 +1,26 @@
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+#root {
+ width: 100%;
+ min-height: 100vh;
+ padding: 20px;
+}
+
+#controls {
+ margin-top: 20px;
+ display: flex;
+ gap: 10px;
+}
+
+@media (prefers-color-scheme: dark) {
+ body {
+ background-color: #121212;
+ color: #ffffff;
+ }
+}
diff --git a/examples/grid-pro/components-react/src/main.tsx b/examples/grid-pro/components-react/src/main.tsx
new file mode 100644
index 0000000..2339d59
--- /dev/null
+++ b/examples/grid-pro/components-react/src/main.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+import './index.css';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/examples/grid-pro/components-react/tsconfig.json b/examples/grid-pro/components-react/tsconfig.json
new file mode 100644
index 0000000..561b165
--- /dev/null
+++ b/examples/grid-pro/components-react/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "lib": [
+ "DOM",
+ "ES2016",
+ "ES2017.Object"
+ ],
+ "jsx": "react-jsx",
+ "module": "ES6",
+ "moduleResolution": "node",
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitThis": true,
+ "noFallthroughCasesInSwitch": true,
+ "skipDefaultLibCheck": true,
+ "skipLibCheck": true,
+ "ignoreDeprecations": "5.0",
+ "allowSyntheticDefaultImports": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/examples/grid-pro/components-react/tsconfig.node.json b/examples/grid-pro/components-react/tsconfig.node.json
new file mode 100644
index 0000000..b940375
--- /dev/null
+++ b/examples/grid-pro/components-react/tsconfig.node.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "types": ["node"]
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/examples/grid-pro/components-react/vite.config.ts b/examples/grid-pro/components-react/vite.config.ts
new file mode 100644
index 0000000..524f9cd
--- /dev/null
+++ b/examples/grid-pro/components-react/vite.config.ts
@@ -0,0 +1,29 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { resolve, dirname } from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: [
+ {
+ find: '@highcharts/grid-pro-react',
+ replacement: resolve(__dirname, '../../../packages/grid-pro-react/src/index.ts')
+ },
+ {
+ find: '@highcharts/grid-shared-react',
+ replacement: resolve(__dirname, '../../../packages/grid-shared-react/src/index.ts')
+ },
+ {
+ find: /^@highcharts\/grid-pro(\/.*)?$/,
+ replacement: resolve(__dirname, 'node_modules/@highcharts/grid-pro$1')
+ }
+ ]
+ },
+ server: {
+ port: 3002
+ }
+});
diff --git a/examples/grid-pro/minimal-nextjs/app/page.tsx b/examples/grid-pro/minimal-nextjs/app/page.tsx
index 0397a91..28cbb2b 100644
--- a/examples/grid-pro/minimal-nextjs/app/page.tsx
+++ b/examples/grid-pro/minimal-nextjs/app/page.tsx
@@ -65,7 +65,12 @@ export default function Home() {
return (
<>
-
+
>
);
diff --git a/examples/grid-pro/minimal-react/src/App.tsx b/examples/grid-pro/minimal-react/src/App.tsx
index 8abbbe0..0e7b1aa 100644
--- a/examples/grid-pro/minimal-react/src/App.tsx
+++ b/examples/grid-pro/minimal-react/src/App.tsx
@@ -55,7 +55,12 @@ function App() {
return (
<>
-
+
>
);
diff --git a/package.json b/package.json
index 5d1eb21..cdde1fa 100644
--- a/package.json
+++ b/package.json
@@ -1,36 +1,39 @@
{
- "name": "highcharts-grid-react",
- "description": "Monorepo for Highcharts Grid Pro & Lite React libraries.",
- "private": true,
- "type": "module",
- "scripts": {
- "test": "vitest run",
- "test:watch": "vitest",
- "pretest:e2e": "pnpm build",
- "test:e2e": "vitest run --config vitest.e2e.config.ts",
- "test:all": "pnpm test && pnpm test:e2e",
- "check": "pnpm lint && pnpm test",
- "build": "pnpm -r --filter './packages/*' run build",
- "lint": "pnpm -r --filter './packages/*' run lint",
- "clean": "pnpm -r --filter './{packages,examples}/*' run clean && rimraf node_modules",
- "release:preflight": "pnpm check && pnpm build",
- "release:prepare": "node scripts/release.js",
- "release": "pnpm release:preflight && pnpm publish -r --access public"
- },
- "packageManager": "pnpm@10.27.0",
- "devDependencies": {
- "@eslint/js": "^9.39.1",
- "@stylistic/eslint-plugin": "^5.6.1",
- "@testing-library/react": "^16.3.1",
- "@types/node": "^20.0.0",
- "@vitest/browser": "^4.0.16",
- "@vitest/browser-playwright": "^4.0.16",
- "eslint": "^9.39.1",
- "globals": "^17.0.0",
- "playwright": "^1.57.0",
- "rimraf": "^6.1.2",
- "typescript": "^5.9.3",
- "typescript-eslint": "^8.48.0",
- "vitest": "^4.0.16"
- }
+ "name": "highcharts-grid-react",
+ "description": "Monorepo for Highcharts Grid Pro & Lite React libraries.",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "test": "vitest run",
+ "pretest": "pnpm build",
+ "test:watch": "vitest",
+ "pretest:e2e": "pnpm build",
+ "test:e2e": "vitest run --config vitest.e2e.config.ts",
+ "test:all": "pnpm test && pnpm test:e2e",
+ "check": "pnpm lint && pnpm test",
+ "build": "pnpm -r --filter './packages/*' run build",
+ "lint": "eslint packages examples scripts --ext .ts,.tsx,.js",
+ "clean": "pnpm -r --filter './{packages,examples}/*' run clean && rimraf node_modules",
+ "release:preflight": "pnpm check && pnpm build",
+ "release:prepare": "node scripts/release.js",
+ "release": "pnpm release:preflight && pnpm publish -r --access public",
+ "prepare": "husky"
+ },
+ "packageManager": "pnpm@10.27.0",
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@stylistic/eslint-plugin": "^5.6.1",
+ "@testing-library/react": "^16.3.1",
+ "@types/node": "^20.0.0",
+ "@vitest/browser": "^4.0.16",
+ "@vitest/browser-playwright": "^4.0.16",
+ "eslint": "^9.39.1",
+ "globals": "^17.0.0",
+ "husky": "^9.1.7",
+ "playwright": "^1.57.0",
+ "rimraf": "^6.1.2",
+ "typescript": "^5.9.3",
+ "typescript-eslint": "^8.48.0",
+ "vitest": "^4.0.16"
+ }
}
diff --git a/packages/grid-lite-react/src/Grid.tsx b/packages/grid-lite-react/src/Grid.tsx
index f4d21fb..25ce188 100644
--- a/packages/grid-lite-react/src/Grid.tsx
+++ b/packages/grid-lite-react/src/Grid.tsx
@@ -9,12 +9,29 @@
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 } = props;
+ const { gridOptions, columnKey } = useDeclarativeGridOptions(
+ children,
+ options,
+ (childOptions, opts) => buildGridOptions(childOptions, opts)
+ );
+
+ 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..548f8b8
--- /dev/null
+++ b/packages/grid-lite-react/src/utils/buildGridOptions.ts
@@ -0,0 +1,25 @@
+/**
+ * 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.
+ */
+export function buildGridOptions(
+ childOptions: Record,
+ options?: Options
+): Options {
+ return merge(
+ normalizeChildOptions(childOptions),
+ options ?? {}
+ ) 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-pro-react/src/Grid.tsx b/packages/grid-pro-react/src/Grid.tsx
index 398edea..cbd0d46 100644
--- a/packages/grid-pro-react/src/Grid.tsx
+++ b/packages/grid-pro-react/src/Grid.tsx
@@ -9,12 +9,37 @@
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 { gridKey, gridRef, children, options, callback } = props;
+ const { gridOptions, columnKey } = useDeclarativeGridOptions(
+ children,
+ options,
+ (childOptions, opts) => buildGridOptions(
+ 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..4cef6ab
--- /dev/null
+++ b/packages/grid-pro-react/src/utils/buildGridOptions.ts
@@ -0,0 +1,42 @@
+/**
+ * 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.
+ */
+export function buildGridOptions(
+ gridKey: string,
+ childOptions: Record,
+ options: GridProOptions | undefined,
+ props: GridProProps
+): GridProOptions {
+ const declarativeOptions = mergePaginationEventProps(
+ mergeColumnEventProps(normalizeChildOptions(childOptions))
+ );
+ const result = merge(
+ true,
+ {},
+ merge(declarativeOptions, options ?? {}),
+ 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..0dcab05
--- /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 === undefined;
+
+ 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..08a0bcf
--- /dev/null
+++ b/packages/grid-pro-react/src/utils/mappers/grid/gridOptions.ts
@@ -0,0 +1,110 @@
+/**
+ * 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/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,
+ ...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..b3dcde3
--- /dev/null
+++ b/packages/grid-pro-react/tests/mappers/gridOptions.test.tsx
@@ -0,0 +1,96 @@
+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(),
+ undefined,
+ { gridKey: 'GRID-KEY' } as GridProProps
+ );
+
+ expect(options.gridKey).toBe('GRID-KEY');
+ expect(options.columns).toEqual([{ id: 'name' }]);
+ });
+});
+
+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',
+ ...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..4d51c20 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,11 @@ export interface GridProps {
/**
* Grid configuration options
*/
- options: TOptions;
+ options?: TOptions;
+ /**
+ * Declarative option components (e.g. Caption) passed as children.
+ */
+ children?: ReactNode;
/**
* Optional ref to access the grid instance
*/
@@ -45,11 +49,10 @@ 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;
}
export const BaseGrid = forwardRef(function BaseGrid(
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..d8569ba
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/columns/ColumnDefaults.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 { ColumnOptionsProps } from './columnProps';
+
+export function ColumnDefaults(_props: ColumnOptionsProps) {
+ 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..36984b3
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/index.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
+ *
+ */
+
+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 { 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..a5f9a93
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/pagination/paginationProps.ts
@@ -0,0 +1,57 @@
+/**
+ * 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;
+ /**
+ * 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..e9f5bd8 100644
--- a/packages/grid-shared-react/src/index.ts
+++ b/packages/grid-shared-react/src/index.ts
@@ -12,4 +12,33 @@ 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 { normalizeChildOptions } from './utils/normalizeChildOptions';
+export { useDeclarativeGridOptions } from './hooks/useDeclarativeGridOptions';
+export type {
+ CaptionProps,
+ DescriptionProps,
+ DataProps,
+ DataColumns,
+ DataColumnValue,
+ ColumnProps,
+ ColumnOptionsProps,
+ 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..03c9f03
--- /dev/null
+++ b/packages/grid-shared-react/src/utils/mappers/pagination/paginationOptions.ts
@@ -0,0 +1,83 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+export function normalizePaginationOptions(
+ props: Record
+): Record {
+ const {
+ pageInfo,
+ pageSizeSelector,
+ pageSizeOptions,
+ pageButtons,
+ pageButtonsCount,
+ firstLast,
+ previousNext,
+ enabled,
+ page,
+ pageSize,
+ align,
+ ...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;
+ }
+
+ const controls: Record = {};
+
+ if (pageInfo !== void 0) {
+ controls.pageInfo = pageInfo;
+ }
+
+ if (pageSizeSelector === false) {
+ controls.pageSizeSelector = false;
+ } else if (pageSizeOptions !== void 0) {
+ controls.pageSizeSelector = {
+ enabled: true,
+ options: pageSizeOptions
+ };
+ } else if (pageSizeSelector !== void 0) {
+ controls.pageSizeSelector = pageSizeSelector;
+ }
+
+ 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 };
+}
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..ef45c01
--- /dev/null
+++ b/packages/grid-shared-react/src/utils/normalizeChildOptions.ts
@@ -0,0 +1,48 @@
+/**
+ * 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 { 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`.
+ */
+export function normalizeChildOptions(
+ raw: Record
+): Record {
+ const result = { ...raw };
+
+ if (isObject(result.columnDefaults)) {
+ result.columnDefaults = normalizeColumnOptions({
+ ...result.columnDefaults
+ });
+ }
+
+ if (Array.isArray(result.columns)) {
+ result.columns = result.columns.map((column) => (
+ isObject(column) ? normalizeColumnOptions({ ...column }) : column
+ ));
+ }
+
+ 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;
+}
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 (
<>
-
+