diff --git a/eslint.config.js b/eslint.config.js
index a14c2bd..6be1776 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -17,6 +17,9 @@ 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 }],
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..5894db3
--- /dev/null
+++ b/examples/grid-lite/components-react/src/App.tsx
@@ -0,0 +1,52 @@
+import { useState, useRef } from 'react';
+import {
+ type GridInstance,
+ type GridRefHandle,
+ type GridOptions,
+ Grid,
+ Caption,
+ Description
+} from '@highcharts/grid-lite-react';
+
+function App() {
+ // const grid = useRef | null>(null);
+ const [description, setDescription] = useState('Grid Description');
+ const onSetDescriptionClick = () => {
+ setDescription('This is a new description');
+ // console.info('(ref) grid:', grid.current?.grid);
+ };
+
+ const [options] = useState({
+ dataTable: {
+ columns: {
+ 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]
+ }
+ }
+ });
+
+ return (
+ <>
+ console.info('(callback) grid:', grid)}
+ >
+ Grid Caption
+ {description}
+ { /*
+
+
+ Grid Cell |
+
+ */}
+ {/* Grid Pagination */}
+
+
+ >
+ );
+}
+
+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..04bacd8
--- /dev/null
+++ b/examples/grid-lite/components-react/src/index.css
@@ -0,0 +1,26 @@
+* {
+ 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;
+}
+
+@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..adf42ba
--- /dev/null
+++ b/examples/grid-lite/components-react/vite.config.ts
@@ -0,0 +1,22 @@
+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(\/.*)?$/,
+ replacement: resolve(__dirname, 'node_modules/@highcharts/grid-lite$1')
+ }
+ ]
+ },
+ server: {
+ port: 3000
+ }
+});
+
diff --git a/packages/grid-lite-react/src/Grid.tsx b/packages/grid-lite-react/src/Grid.tsx
index f4d21fb..f7851aa 100644
--- a/packages/grid-lite-react/src/Grid.tsx
+++ b/packages/grid-lite-react/src/Grid.tsx
@@ -7,14 +7,23 @@
*
*/
+import { useMemo } from 'react';
import {
BaseGrid,
- GridProps
+ GridProps,
+ getChildProps
} from '@highcharts/grid-shared-react';
+import { merge } from '@highcharts/grid-lite/es-modules/Shared/Utilities.js';
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';
-export default function GridLite({ options, gridRef, callback }: GridProps) {
- return ;
+export default function GridLite(props: GridProps) {
+ const { gridRef, children, options, ...gridProps } = props;
+ const gridOptions = useMemo(
+ () => merge(getChildProps(children), options ?? {}) as Options,
+ [children, options]
+ );
+
+ return ;
}
diff --git a/packages/grid-lite-react/src/index.ts b/packages/grid-lite-react/src/index.ts
index 93cb0e5..695362c 100644
--- a/packages/grid-lite-react/src/index.ts
+++ b/packages/grid-lite-react/src/index.ts
@@ -11,6 +11,6 @@ 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, Description } from '@highcharts/grid-shared-react';
+export type { GridInstance, GridRefHandle, CaptionProps, DescriptionProps } from '@highcharts/grid-shared-react';
export type GridOptions = GridLite.Options;
diff --git a/packages/grid-pro-react/src/Grid.tsx b/packages/grid-pro-react/src/Grid.tsx
index 398edea..fc7f2ba 100644
--- a/packages/grid-pro-react/src/Grid.tsx
+++ b/packages/grid-pro-react/src/Grid.tsx
@@ -7,14 +7,23 @@
*
*/
+import { useMemo } from 'react';
import {
BaseGrid,
- GridProps
+ GridProps,
+ getChildProps
} from '@highcharts/grid-shared-react';
+import { merge } from '@highcharts/grid-pro/es-modules/Shared/Utilities.js';
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';
-export default function GridPro({ options, gridRef, callback }: GridProps) {
- return ;
+export default function GridPro(props: GridProps) {
+ const { gridRef, children, options, ...gridProps } = props;
+ const gridOptions = useMemo(
+ () => merge(getChildProps(children), options ?? {}) as Options,
+ [children, options]
+ );
+
+ return ;
}
diff --git a/packages/grid-pro-react/src/index.ts b/packages/grid-pro-react/src/index.ts
index a44be66..84f1e66 100644
--- a/packages/grid-pro-react/src/index.ts
+++ b/packages/grid-pro-react/src/index.ts
@@ -11,6 +11,6 @@ import GridPro from '@highcharts/grid-pro';
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 { Caption, Description } from '@highcharts/grid-shared-react';
+export type { GridInstance, GridRefHandle, CaptionProps, DescriptionProps } from '@highcharts/grid-shared-react';
export type GridOptions = GridPro.Options;
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..9440edc
--- /dev/null
+++ b/packages/grid-shared-react/src/components/BaseGridOptions.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
+ *
+ */
+
+/**
+ * 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/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/index.ts b/packages/grid-shared-react/src/components/options/index.ts
new file mode 100644
index 0000000..5cf87d4
--- /dev/null
+++ b/packages/grid-shared-react/src/components/options/index.ts
@@ -0,0 +1,13 @@
+/**
+ * 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 { Description } from './description';
+export type { DescriptionProps } from './description';
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..adf07cf 100644
--- a/packages/grid-shared-react/src/hooks/useGrid.ts
+++ b/packages/grid-shared-react/src/hooks/useGrid.ts
@@ -8,7 +8,6 @@
*/
import { useEffect, RefObject, useRef } from 'react';
-import { BaseGridProps } from '../components/BaseGrid';
/**
* Interface describing the shape of a Grid instance returned by Grid.grid()
@@ -25,11 +24,14 @@ 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 +42,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.
@@ -73,7 +75,7 @@ export function useGrid({
try {
// 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);
@@ -86,9 +88,9 @@ export function useGrid({
currGridRef.current = grid;
// Apply any pending options that came in while we were initializing
- if (pendingOptionsRef.current) {
+ if (pendingOptionsRef.current !== void 0) {
grid.update(pendingOptionsRef.current, true);
- pendingOptionsRef.current = null;
+ pendingOptionsRef.current = void 0;
}
callbackRef.current?.(grid);
@@ -115,6 +117,10 @@ 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);
diff --git a/packages/grid-shared-react/src/index.ts b/packages/grid-shared-react/src/index.ts
index 6a47b1b..3672450 100644
--- a/packages/grid-shared-react/src/index.ts
+++ b/packages/grid-shared-react/src/index.ts
@@ -12,4 +12,7 @@ import { GridType, GridInstance } from './hooks/useGrid';
import type { GridProps, GridRefHandle } from './components/BaseGrid';
export { BaseGrid };
+export { Caption, Description } from './components/options';
+export { getChildProps } from './utils/getChildProps';
+export type { CaptionProps, DescriptionProps } 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..3d11b1a
--- /dev/null
+++ b/packages/grid-shared-react/src/utils/getChildProps.ts
@@ -0,0 +1,215 @@
+/**
+ * Grid React integration.
+ * Copyright (c) 2025, Highsoft
+ *
+ * A valid license is required for using this software.
+ * See highcharts.com/license
+ *
+ */
+
+import { isValidElement, ReactElement, ReactNode } from 'react';
+import type { BaseGridOptionsComponent, BaseGridOptions } from '../components/BaseGridOptions';
+
+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 isObject(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
+
+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
+ };
+}
+
+export function getChildProps(children: ReactNode): Record {
+ const optionsFromChildren: Record = {};
+ const resolvedChildren = (Array.isArray(children) ? children.flat() : [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 optionParent = optionsFromChildren[meta.gridOption] ?? (
+ optionsFromChildren[meta.gridOption] = meta.isArrayType ? [] : {}
+ );
+ const parentIsArray = Array.isArray(optionParent);
+ const insertInto = parentIsArray ? {} : optionParent as Record;
+ const childProps = getChildPropsFromElement(child);
+ const { children: childChildren, ...props } = childProps;
+
+ 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) {
+ (optionsFromChildren[meta.gridOption] as unknown[]).push(insertInto);
+ }
+ }
+
+ for (const child of resolvedChildren) {
+ handleChild(child);
+ }
+
+ return optionsFromChildren;
+}
+
+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 rendered = (child.type as (props: Record) => ReactNode)(
+ getChildPropsFromElement(child)
+ );
+
+ if (isReactElement(rendered) && getOptionComponent(rendered.type)) {
+ return rendered;
+ }
+
+ return null;
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 05e60b9..ff81d18 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -48,6 +48,37 @@ importers:
specifier: ^4.0.16
version: 4.0.16(@types/node@20.19.26)(@vitest/browser-playwright@4.0.16)(jsdom@27.4.0)
+ examples/grid-lite/components-react:
+ dependencies:
+ '@highcharts/grid-lite':
+ specifier: '>=3.0.0'
+ version: 3.0.0
+ '@highcharts/grid-lite-react':
+ specifier: workspace:*
+ version: link:../../../packages/grid-lite-react
+ react:
+ specifier: '>=18'
+ version: 19.2.1
+ react-dom:
+ specifier: '>=18'
+ version: 19.2.1(react@19.2.1)
+ devDependencies:
+ '@types/react':
+ specifier: '>=18'
+ version: 19.2.7
+ '@types/react-dom':
+ specifier: '>=18'
+ version: 19.2.3(@types/react@19.2.7)
+ '@vitejs/plugin-react':
+ specifier: ^4.2.0
+ version: 4.7.0(vite@5.4.21(@types/node@20.19.26))
+ typescript:
+ specifier: ^5.0.0
+ version: 5.9.3
+ vite:
+ specifier: ^5.0.0
+ version: 5.4.21(@types/node@20.19.26)
+
examples/grid-lite/minimal-nextjs:
dependencies:
'@highcharts/grid-lite':