From ea622a5a3625c6a657096dd9ded4cbbcbfebac1f Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Mon, 22 Jun 2026 14:14:38 +0200 Subject: [PATCH 1/6] Init demo. --- .../grid-lite/components-react/index.html | 13 +++++ .../grid-lite/components-react/package.json | 26 ++++++++++ .../grid-lite/components-react/src/App.tsx | 48 +++++++++++++++++++ .../grid-lite/components-react/src/index.css | 26 ++++++++++ .../grid-lite/components-react/src/main.tsx | 11 +++++ .../grid-lite/components-react/tsconfig.json | 28 +++++++++++ .../components-react/tsconfig.node.json | 12 +++++ .../grid-lite/components-react/vite.config.ts | 22 +++++++++ 8 files changed, 186 insertions(+) create mode 100644 examples/grid-lite/components-react/index.html create mode 100644 examples/grid-lite/components-react/package.json create mode 100644 examples/grid-lite/components-react/src/App.tsx create mode 100644 examples/grid-lite/components-react/src/index.css create mode 100644 examples/grid-lite/components-react/src/main.tsx create mode 100644 examples/grid-lite/components-react/tsconfig.json create mode 100644 examples/grid-lite/components-react/tsconfig.node.json create mode 100644 examples/grid-lite/components-react/vite.config.ts 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..9151ef7 --- /dev/null +++ b/examples/grid-lite/components-react/src/App.tsx @@ -0,0 +1,48 @@ +import { useState, useRef } from 'react'; +import { + type GridInstance, + type GridOptions, + type GridRefHandle, + Grid +} from '@highcharts/grid-lite-react'; + +function App() { + 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] + } + }, + caption: { + text: 'Grid Lite' + }, + pagination: { + enabled: true, + pageSize: 3, + controls: { + pageSizeSelector: true, + pageButtons: true + } + } + }); + const grid = useRef | null>(null); + + const onButtonClick = () => { + console.info('(ref) grid:', grid.current?.grid); + }; + const onGridCallback = (grid: GridInstance) => { + console.info('(callback) grid:', grid); + }; + + return ( + <> + + + + ); +} + +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 + } +}); + From 70e0846f94ba2d9fcb870a30642ff34fb021a5fa Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Mon, 29 Jun 2026 14:16:47 +0200 Subject: [PATCH 2/6] Options attrib as optional. --- .../grid-lite/components-react/src/App.tsx | 34 ++++++++----------- .../src/components/BaseGrid.tsx | 2 +- .../grid-shared-react/src/hooks/useGrid.ts | 14 +++++--- pnpm-lock.yaml | 31 +++++++++++++++++ 4 files changed, 55 insertions(+), 26 deletions(-) diff --git a/examples/grid-lite/components-react/src/App.tsx b/examples/grid-lite/components-react/src/App.tsx index 9151ef7..eaa04d9 100644 --- a/examples/grid-lite/components-react/src/App.tsx +++ b/examples/grid-lite/components-react/src/App.tsx @@ -15,32 +15,26 @@ function App() { city: ['New York', 'Oslo', 'Paris', 'Tokyo', 'London'], salary: [50000, 60000, 70000, 80000, 90000] } - }, - caption: { - text: 'Grid Lite' - }, - pagination: { - enabled: true, - pageSize: 3, - controls: { - pageSizeSelector: true, - pageButtons: true - } } }); - const grid = useRef | null>(null); + // const grid = useRef | null>(null); - const onButtonClick = () => { - console.info('(ref) grid:', grid.current?.grid); - }; - const onGridCallback = (grid: GridInstance) => { - console.info('(callback) grid:', grid); - }; + // const onButtonClick = () => { + // console.info('(ref) grid:', grid.current?.grid); + // }; + // const onGridCallback = (grid: GridInstance) => { + // console.info('(callback) grid:', grid); + // }; return ( <> - - + + + {/* */} ); } diff --git a/packages/grid-shared-react/src/components/BaseGrid.tsx b/packages/grid-shared-react/src/components/BaseGrid.tsx index 437459e..655de72 100644 --- a/packages/grid-shared-react/src/components/BaseGrid.tsx +++ b/packages/grid-shared-react/src/components/BaseGrid.tsx @@ -31,7 +31,7 @@ export interface GridProps { /** * Grid configuration options */ - options: TOptions; + options?: TOptions; /** * Optional ref to access the grid instance */ diff --git a/packages/grid-shared-react/src/hooks/useGrid.ts b/packages/grid-shared-react/src/hooks/useGrid.ts index 327e3fb..bfbf566 100644 --- a/packages/grid-shared-react/src/hooks/useGrid.ts +++ b/packages/grid-shared-react/src/hooks/useGrid.ts @@ -25,7 +25,7 @@ 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 { @@ -40,7 +40,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 +73,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 +86,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 +115,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/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': From 360a72fd78306ac02688dcba823b93f3e185e8c5 Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Mon, 29 Jun 2026 14:56:14 +0200 Subject: [PATCH 3/6] Added children support in Grid. --- .../grid-lite/components-react/src/App.tsx | 31 +++++++++++++------ packages/grid-lite-react/src/Grid.tsx | 5 +-- packages/grid-pro-react/src/Grid.tsx | 5 +-- .../src/components/BaseGrid.tsx | 15 +++++++-- 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/examples/grid-lite/components-react/src/App.tsx b/examples/grid-lite/components-react/src/App.tsx index eaa04d9..744ea89 100644 --- a/examples/grid-lite/components-react/src/App.tsx +++ b/examples/grid-lite/components-react/src/App.tsx @@ -1,12 +1,21 @@ import { useState, useRef } from 'react'; import { - type GridInstance, + // type GridInstance, + // type GridRefHandle, type GridOptions, - type GridRefHandle, Grid } from '@highcharts/grid-lite-react'; function App() { + /* const grid = useRef | null>(null); + + const onButtonClick = () => { + console.info('(ref) grid:', grid.current?.grid); + }; + const onGridCallback = (grid: GridInstance) => { + console.info('(callback) grid:', grid); + };*/ + const [options] = useState({ dataTable: { columns: { @@ -17,14 +26,6 @@ function App() { } } }); - // const grid = useRef | null>(null); - - // const onButtonClick = () => { - // console.info('(ref) grid:', grid.current?.grid); - // }; - // const onGridCallback = (grid: GridInstance) => { - // console.info('(callback) grid:', grid); - // }; return ( <> @@ -33,6 +34,16 @@ function App() { // gridRef={grid} // callback={onGridCallback} > +
Whatever
+ {/* Grid Caption */} + {/* Grid Description + + +
Grid Header
+ Grid Cell +
+
*/} + {/* Grid Pagination */}
{/* */} diff --git a/packages/grid-lite-react/src/Grid.tsx b/packages/grid-lite-react/src/Grid.tsx index f4d21fb..4b918d2 100644 --- a/packages/grid-lite-react/src/Grid.tsx +++ b/packages/grid-lite-react/src/Grid.tsx @@ -15,6 +15,7 @@ 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, ...gridProps } = props; + return ; } diff --git a/packages/grid-pro-react/src/Grid.tsx b/packages/grid-pro-react/src/Grid.tsx index 398edea..1e55204 100644 --- a/packages/grid-pro-react/src/Grid.tsx +++ b/packages/grid-pro-react/src/Grid.tsx @@ -15,6 +15,7 @@ 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, ...gridProps } = props; + return ; } diff --git a/packages/grid-shared-react/src/components/BaseGrid.tsx b/packages/grid-shared-react/src/components/BaseGrid.tsx index 655de72..6014d73 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, @@ -32,6 +32,10 @@ export interface GridProps { * Grid configuration options */ options?: TOptions; + /** + * Optional React children rendered inside the Grid wrapper. + */ + children?: ReactNode; /** * Optional ref to access the grid instance */ @@ -56,7 +60,7 @@ export const BaseGrid = forwardRef(function BaseGrid( props: BaseGridProps, ref: ForwardedRef> ) { - const { options, Grid, callback } = props; + const { options, Grid, callback, children } = props; const containerRef = useRef(null); const currGridRef = useGrid({ @@ -76,5 +80,10 @@ export const BaseGrid = forwardRef(function BaseGrid( [] ); - return
; + return ( +
+ {children} +
+
+ ); }); From 651d8aeb2010115fa03d9125568fa3d5f29c13be Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Tue, 30 Jun 2026 13:28:41 +0200 Subject: [PATCH 4/6] Created Caption.jsx component. --- .../grid-lite/components-react/src/App.tsx | 15 +- packages/grid-lite-react/src/Grid.tsx | 14 +- packages/grid-lite-react/src/index.ts | 4 +- packages/grid-pro-react/src/Grid.tsx | 14 +- packages/grid-pro-react/src/index.ts | 4 +- .../src/components/BaseGrid.tsx | 18 +- .../src/components/BaseGridOptions.ts | 32 +++ .../components/options/caption/Caption.tsx | 34 +++ .../src/components/options/caption/index.ts | 11 + .../src/components/options/index.ts | 11 + .../src/hooks/useGrid.test.tsx | 5 +- .../grid-shared-react/src/hooks/useGrid.ts | 6 +- packages/grid-shared-react/src/index.ts | 3 + .../src/utils/getChildProps.ts | 215 ++++++++++++++++++ 14 files changed, 353 insertions(+), 33 deletions(-) create mode 100644 packages/grid-shared-react/src/components/BaseGridOptions.ts create mode 100644 packages/grid-shared-react/src/components/options/caption/Caption.tsx create mode 100644 packages/grid-shared-react/src/components/options/caption/index.ts create mode 100644 packages/grid-shared-react/src/components/options/index.ts create mode 100644 packages/grid-shared-react/src/utils/getChildProps.ts diff --git a/examples/grid-lite/components-react/src/App.tsx b/examples/grid-lite/components-react/src/App.tsx index 744ea89..d593199 100644 --- a/examples/grid-lite/components-react/src/App.tsx +++ b/examples/grid-lite/components-react/src/App.tsx @@ -1,9 +1,10 @@ import { useState, useRef } from 'react'; import { - // type GridInstance, + type GridInstance, // type GridRefHandle, type GridOptions, - Grid + Grid, + Caption } from '@highcharts/grid-lite-react'; function App() { @@ -11,10 +12,11 @@ function App() { const onButtonClick = () => { console.info('(ref) grid:', grid.current?.grid); - }; + }; */ + const onGridCallback = (grid: GridInstance) => { console.info('(callback) grid:', grid); - };*/ + }; const [options] = useState({ dataTable: { @@ -32,10 +34,9 @@ function App() { -
Whatever
- {/* Grid Caption */} + Grid Caption {/* Grid Description diff --git a/packages/grid-lite-react/src/Grid.tsx b/packages/grid-lite-react/src/Grid.tsx index 4b918d2..f7851aa 100644 --- a/packages/grid-lite-react/src/Grid.tsx +++ b/packages/grid-lite-react/src/Grid.tsx @@ -7,15 +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(props: GridProps) { - const { gridRef, ...gridProps } = props; - return ; + 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..a96ed99 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 } from '@highcharts/grid-shared-react'; +export type { GridInstance, GridRefHandle, CaptionProps } 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 1e55204..fc7f2ba 100644 --- a/packages/grid-pro-react/src/Grid.tsx +++ b/packages/grid-pro-react/src/Grid.tsx @@ -7,15 +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(props: GridProps) { - const { gridRef, ...gridProps } = props; - return ; + 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..d628c95 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 } from '@highcharts/grid-shared-react'; +export type { GridInstance, GridRefHandle, CaptionProps } 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 6014d73..4d51c20 100644 --- a/packages/grid-shared-react/src/components/BaseGrid.tsx +++ b/packages/grid-shared-react/src/components/BaseGrid.tsx @@ -33,7 +33,7 @@ export interface GridProps { */ options?: TOptions; /** - * Optional React children rendered inside the Grid wrapper. + * Declarative option components (e.g. Caption) passed as children. */ children?: ReactNode; /** @@ -49,18 +49,17 @@ 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( props: BaseGridProps, ref: ForwardedRef> ) { - const { options, Grid, callback, children } = props; + const { options, Grid, callback } = props; const containerRef = useRef(null); const currGridRef = useGrid({ @@ -80,10 +79,5 @@ export const BaseGrid = forwardRef(function BaseGrid( [] ); - return ( -
- {children} -
-
- ); + return
; }); diff --git a/packages/grid-shared-react/src/components/BaseGridOptions.ts b/packages/grid-shared-react/src/components/BaseGridOptions.ts new file mode 100644 index 0000000..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..e8ffaef --- /dev/null +++ b/packages/grid-shared-react/src/components/options/caption/Caption.tsx @@ -0,0 +1,34 @@ +/** + * 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): null; +export function Caption(): null { + 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/index.ts b/packages/grid-shared-react/src/components/options/index.ts new file mode 100644 index 0000000..f4822ce --- /dev/null +++ b/packages/grid-shared-react/src/components/options/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/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 bfbf566..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() @@ -28,8 +27,11 @@ export interface GridType { 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({ diff --git a/packages/grid-shared-react/src/index.ts b/packages/grid-shared-react/src/index.ts index 6a47b1b..445d13c 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 } from './components/options'; +export { getChildProps } from './utils/getChildProps'; +export type { CaptionProps } 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; +} From dd9055ab41ba59105b8b5b4cbd0d51a634ac9ad5 Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Tue, 30 Jun 2026 13:43:43 +0200 Subject: [PATCH 5/6] Fixed overloads. --- eslint.config.js | 3 +++ .../src/components/options/caption/Caption.tsx | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) 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/packages/grid-shared-react/src/components/options/caption/Caption.tsx b/packages/grid-shared-react/src/components/options/caption/Caption.tsx index e8ffaef..99c9199 100644 --- a/packages/grid-shared-react/src/components/options/caption/Caption.tsx +++ b/packages/grid-shared-react/src/components/options/caption/Caption.tsx @@ -21,8 +21,7 @@ export interface CaptionProps { children?: ReactNode; } -export function Caption(_props: CaptionProps): null; -export function Caption(): null { +export function Caption(_props: CaptionProps) { return null; } From 380c87821fac2c5515f3301c6f91ee5360975831 Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Tue, 30 Jun 2026 14:13:13 +0200 Subject: [PATCH 6/6] Added description component. --- .../grid-lite/components-react/src/App.tsx | 26 ++++++++--------- packages/grid-lite-react/src/index.ts | 4 +-- packages/grid-pro-react/src/index.ts | 4 +-- .../options/description/Description.tsx | 29 +++++++++++++++++++ .../components/options/description/index.ts | 11 +++++++ .../src/components/options/index.ts | 2 ++ packages/grid-shared-react/src/index.ts | 4 +-- 7 files changed, 60 insertions(+), 20 deletions(-) create mode 100644 packages/grid-shared-react/src/components/options/description/Description.tsx create mode 100644 packages/grid-shared-react/src/components/options/description/index.ts diff --git a/examples/grid-lite/components-react/src/App.tsx b/examples/grid-lite/components-react/src/App.tsx index d593199..5894db3 100644 --- a/examples/grid-lite/components-react/src/App.tsx +++ b/examples/grid-lite/components-react/src/App.tsx @@ -1,21 +1,19 @@ import { useState, useRef } from 'react'; import { type GridInstance, - // type GridRefHandle, + type GridRefHandle, type GridOptions, Grid, - Caption + Caption, + Description } from '@highcharts/grid-lite-react'; function App() { - /* const grid = useRef | null>(null); - - const onButtonClick = () => { - console.info('(ref) grid:', grid.current?.grid); - }; */ - - const onGridCallback = (grid: GridInstance) => { - console.info('(callback) grid:', grid); + // 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({ @@ -34,11 +32,11 @@ function App() { console.info('(callback) grid:', grid)} > Grid Caption - {/* Grid Description - + {description} + { /*
Grid Header
Grid Cell @@ -46,7 +44,7 @@ function App() {
*/} {/* Grid Pagination */}
- {/* */} + ); } diff --git a/packages/grid-lite-react/src/index.ts b/packages/grid-lite-react/src/index.ts index a96ed99..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 { Caption } from '@highcharts/grid-shared-react'; -export type { GridInstance, GridRefHandle, CaptionProps } 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/index.ts b/packages/grid-pro-react/src/index.ts index d628c95..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 { Caption } from '@highcharts/grid-shared-react'; -export type { GridInstance, GridRefHandle, CaptionProps } 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/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 index f4822ce..5cf87d4 100644 --- a/packages/grid-shared-react/src/components/options/index.ts +++ b/packages/grid-shared-react/src/components/options/index.ts @@ -9,3 +9,5 @@ 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/index.ts b/packages/grid-shared-react/src/index.ts index 445d13c..3672450 100644 --- a/packages/grid-shared-react/src/index.ts +++ b/packages/grid-shared-react/src/index.ts @@ -12,7 +12,7 @@ import { GridType, GridInstance } from './hooks/useGrid'; import type { GridProps, GridRefHandle } from './components/BaseGrid'; export { BaseGrid }; -export { Caption } from './components/options'; +export { Caption, Description } from './components/options'; export { getChildProps } from './utils/getChildProps'; -export type { CaptionProps } from './components/options'; +export type { CaptionProps, DescriptionProps } from './components/options'; export type { GridType, GridInstance, GridProps, GridRefHandle };