From ea622a5a3625c6a657096dd9ded4cbbcbfebac1f Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Mon, 22 Jun 2026 14:14:38 +0200 Subject: [PATCH 01/28] 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 02/28] 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 03/28] 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 04/28] 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 05/28] 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 06/28] 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 }; From ad721bbef4e2c09a7e8bd4e959ee5e98ceff4f27 Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Tue, 30 Jun 2026 15:32:53 +0200 Subject: [PATCH 07/28] Added Data component. --- .../grid-lite/components-react/src/App.tsx | 77 +++++++++++++------ packages/grid-lite-react/src/index.ts | 13 +++- packages/grid-pro-react/src/index.ts | 13 +++- .../src/components/options/data/Data.tsx | 60 +++++++++++++++ .../src/components/options/index.ts | 2 + packages/grid-shared-react/src/index.ts | 4 +- .../src/utils/getChildProps.test.tsx | 50 ++++++++++++ packages/grid-shared-react/tsconfig.json | 2 +- 8 files changed, 191 insertions(+), 30 deletions(-) create mode 100644 packages/grid-shared-react/src/components/options/data/Data.tsx create mode 100644 packages/grid-shared-react/src/utils/getChildProps.test.tsx diff --git a/examples/grid-lite/components-react/src/App.tsx b/examples/grid-lite/components-react/src/App.tsx index d593199..939c0bc 100644 --- a/examples/grid-lite/components-react/src/App.tsx +++ b/examples/grid-lite/components-react/src/App.tsx @@ -1,52 +1,83 @@ import { useState, useRef } from 'react'; import { type GridInstance, - // type GridRefHandle, + type GridRefHandle, type GridOptions, Grid, - Caption + Caption, + Data, + DataTable } from '@highcharts/grid-lite-react'; function App() { - /* const grid = useRef | null>(null); + 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); - }; */ + // 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); }; - 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 ( <> Grid Caption - {/* Grid Description - - + + {/*
Grid Header
Grid Cell -
-
*/} + */} + {/* Grid Description */} {/* Grid Pagination */}
- {/* */} + ); } diff --git a/packages/grid-lite-react/src/index.ts b/packages/grid-lite-react/src/index.ts index a96ed99..abf3c20 100644 --- a/packages/grid-lite-react/src/index.ts +++ b/packages/grid-lite-react/src/index.ts @@ -11,6 +11,15 @@ 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, Data } 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, + DataProps, + DataColumns, + DataColumnValue +} 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..f62dff1 100644 --- a/packages/grid-pro-react/src/index.ts +++ b/packages/grid-pro-react/src/index.ts @@ -11,6 +11,15 @@ 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, Data } from '@highcharts/grid-shared-react'; +export { DataTable, DataConnector } from '@highcharts/grid-pro'; +export { merge } from '@highcharts/grid-pro/es-modules/Shared/Utilities.js'; +export type { + GridInstance, + GridRefHandle, + CaptionProps, + DataProps, + DataColumns, + DataColumnValue +} from '@highcharts/grid-shared-react'; export type GridOptions = GridPro.Options; 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..da24ab3 --- /dev/null +++ b/packages/grid-shared-react/src/components/options/data/Data.tsx @@ -0,0 +1,60 @@ +/** + * 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. + * + * @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/index.ts b/packages/grid-shared-react/src/components/options/index.ts index f4822ce..5f099f9 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 { Data } from './data/Data'; +export type { DataProps, DataColumns, DataColumnValue } from './data/Data'; diff --git a/packages/grid-shared-react/src/index.ts b/packages/grid-shared-react/src/index.ts index 445d13c..ad49409 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, Data } from './components/options'; export { getChildProps } from './utils/getChildProps'; -export type { CaptionProps } from './components/options'; +export type { CaptionProps, DataProps, DataColumns, DataColumnValue } from './components/options'; export type { GridType, GridInstance, GridProps, GridRefHandle }; diff --git a/packages/grid-shared-react/src/utils/getChildProps.test.tsx b/packages/grid-shared-react/src/utils/getChildProps.test.tsx new file mode 100644 index 0000000..1498a4e --- /dev/null +++ b/packages/grid-shared-react/src/utils/getChildProps.test.tsx @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest'; +import { Data } from '../components/options/data/Data'; +import { getChildProps } from './getChildProps'; + +describe('getChildProps', () => { + it('maps Data columns to options.data.columns', () => { + const columns = { + name: ['Alice', 'Bob'], + age: [23, 34] + }; + + expect(getChildProps()).toEqual({ + data: { + columns + } + }); + }); + + it('maps all Data props to options.data', () => { + const columns = { + name: ['Alice'] + }; + const connector = { id: 'csv' }; + const dataTable = { id: 'table-1' }; + + expect( + getChildProps( + + ) + ).toEqual({ + data: { + providerType: 'local', + autogenerateColumns: false, + columns, + connector, + dataTable, + updateOnChange: true, + idColumn: 'id' + } + }); + }); +}); diff --git a/packages/grid-shared-react/tsconfig.json b/packages/grid-shared-react/tsconfig.json index b1b3107..f1d16c9 100644 --- a/packages/grid-shared-react/tsconfig.json +++ b/packages/grid-shared-react/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "../../tsconfig.base.json", - "include": ["src", "tests"] + "include": ["src"] } From 88fe92ac9eb4cbd2067999345e4a9d5450215fa2 Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Wed, 1 Jul 2026 13:29:41 +0200 Subject: [PATCH 08/28] Added Data, Columns and Column components. --- .../grid-lite/components-react/src/App.tsx | 72 +++++++++-- .../grid-lite/components-react/vite.config.ts | 8 ++ packages/grid-lite-react/src/Grid.tsx | 20 ++- packages/grid-lite-react/src/index.ts | 10 +- packages/grid-pro-react/src/Grid.tsx | 20 ++- packages/grid-pro-react/src/index.ts | 10 +- .../src/components/BaseGridOptions.ts | 4 + .../src/components/options/columns/Column.tsx | 21 ++++ .../components/options/columns/Columns.tsx | 20 +++ .../components/options/columns/columnProps.ts | 80 ++++++++++++ .../src/components/options/data/Data.tsx | 11 ++ .../src/components/options/index.ts | 10 ++ .../grid-shared-react/src/hooks/useGrid.ts | 8 +- packages/grid-shared-react/src/index.ts | 15 ++- .../src/utils/getChildProps.ts | 116 +++++++++++++++++- .../src/utils/mappers/columnOptions.ts | 24 ++++ .../src/utils/mappers/mapPrefixedProps.ts | 57 +++++++++ 17 files changed, 478 insertions(+), 28 deletions(-) create mode 100644 packages/grid-shared-react/src/components/options/columns/Column.tsx create mode 100644 packages/grid-shared-react/src/components/options/columns/Columns.tsx create mode 100644 packages/grid-shared-react/src/components/options/columns/columnProps.ts create mode 100644 packages/grid-shared-react/src/utils/mappers/columnOptions.ts create mode 100644 packages/grid-shared-react/src/utils/mappers/mapPrefixedProps.ts diff --git a/examples/grid-lite/components-react/src/App.tsx b/examples/grid-lite/components-react/src/App.tsx index 939c0bc..26cf85e 100644 --- a/examples/grid-lite/components-react/src/App.tsx +++ b/examples/grid-lite/components-react/src/App.tsx @@ -6,7 +6,9 @@ import { Grid, Caption, Data, - DataTable + DataTable, + Columns, + Column } from '@highcharts/grid-lite-react'; function App() { @@ -63,17 +65,73 @@ function App() { Grid Caption - {/* -
Grid Header
- Grid Cell -
*/} + > + + + + + + + +
{/* Grid Description */} {/* Grid Pagination */}
diff --git a/examples/grid-lite/components-react/vite.config.ts b/examples/grid-lite/components-react/vite.config.ts index adf42ba..2e3f28b 100644 --- a/examples/grid-lite/components-react/vite.config.ts +++ b/examples/grid-lite/components-react/vite.config.ts @@ -9,6 +9,14 @@ 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') diff --git a/packages/grid-lite-react/src/Grid.tsx b/packages/grid-lite-react/src/Grid.tsx index f7851aa..aa47803 100644 --- a/packages/grid-lite-react/src/Grid.tsx +++ b/packages/grid-lite-react/src/Grid.tsx @@ -20,10 +20,24 @@ import type { Options } from '@highcharts/grid-lite/es-modules/Grid/Core/Options export default function GridLite(props: GridProps) { const { gridRef, children, options, ...gridProps } = props; + const childOptions = useMemo(() => getChildProps(children), [children]); + const columnKey = useMemo(() => { + const columns = childOptions.columns as Array<{ id?: string }> | undefined; + + return columns?.map((column) => column.id).join('\0') ?? ''; + }, [childOptions]); const gridOptions = useMemo( - () => merge(getChildProps(children), options ?? {}) as Options, - [children, options] + () => merge(childOptions, options ?? {}) as Options, + [childOptions, options] ); - return ; + return ( + + ); } diff --git a/packages/grid-lite-react/src/index.ts b/packages/grid-lite-react/src/index.ts index abf3c20..4d0a248 100644 --- a/packages/grid-lite-react/src/index.ts +++ b/packages/grid-lite-react/src/index.ts @@ -11,7 +11,7 @@ import GridLite from '@highcharts/grid-lite'; export { default as Grid } from './Grid'; export { default as GridLite } from './Grid'; -export { Caption, Data } from '@highcharts/grid-shared-react'; +export { Caption, Data, Columns, Column } 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 { @@ -20,6 +20,12 @@ export type { CaptionProps, DataProps, DataColumns, - DataColumnValue + DataColumnValue, + ColumnsProps, + ColumnProps, + ColumnOptionsProps, + ColumnDataType, + ColumnSortingOrder, + CellValueGetterContext } 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 fc7f2ba..c236cfd 100644 --- a/packages/grid-pro-react/src/Grid.tsx +++ b/packages/grid-pro-react/src/Grid.tsx @@ -20,10 +20,24 @@ import type { Options } from '@highcharts/grid-pro/es-modules/Grid/Core/Options' export default function GridPro(props: GridProps) { const { gridRef, children, options, ...gridProps } = props; + const childOptions = useMemo(() => getChildProps(children), [children]); + const columnKey = useMemo(() => { + const columns = childOptions.columns as Array<{ id?: string }> | undefined; + + return columns?.map((column) => column.id).join('\0') ?? ''; + }, [childOptions]); const gridOptions = useMemo( - () => merge(getChildProps(children), options ?? {}) as Options, - [children, options] + () => merge(childOptions, options ?? {}) as Options, + [childOptions, options] ); - return ; + return ( + + ); } diff --git a/packages/grid-pro-react/src/index.ts b/packages/grid-pro-react/src/index.ts index f62dff1..0cb0930 100644 --- a/packages/grid-pro-react/src/index.ts +++ b/packages/grid-pro-react/src/index.ts @@ -11,7 +11,7 @@ import GridPro from '@highcharts/grid-pro'; export { default as Grid } from './Grid'; export { default as GridPro } from './Grid'; -export { Caption, Data } from '@highcharts/grid-shared-react'; +export { Caption, Data, Columns, Column } from '@highcharts/grid-shared-react'; export { DataTable, DataConnector } from '@highcharts/grid-pro'; export { merge } from '@highcharts/grid-pro/es-modules/Shared/Utilities.js'; export type { @@ -20,6 +20,12 @@ export type { CaptionProps, DataProps, DataColumns, - DataColumnValue + DataColumnValue, + ColumnsProps, + ColumnProps, + ColumnOptionsProps, + ColumnDataType, + ColumnSortingOrder, + CellValueGetterContext } from '@highcharts/grid-shared-react'; export type GridOptions = GridPro.Options; diff --git a/packages/grid-shared-react/src/components/BaseGridOptions.ts b/packages/grid-shared-react/src/components/BaseGridOptions.ts index 9440edc..d35d54c 100644 --- a/packages/grid-shared-react/src/components/BaseGridOptions.ts +++ b/packages/grid-shared-react/src/components/BaseGridOptions.ts @@ -22,6 +22,10 @@ export interface BaseGridOptions { childOption?: string; defaultOptions?: Record; isArrayType?: boolean; + /** + * Special parsing role for column-related components. + */ + role?: 'columnsContainer' | 'column'; } /** 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..f6c1d60 --- /dev/null +++ b/packages/grid-shared-react/src/components/options/columns/Column.tsx @@ -0,0 +1,21 @@ +/** + * 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', + role: 'column', + isArrayType: true +}; diff --git a/packages/grid-shared-react/src/components/options/columns/Columns.tsx b/packages/grid-shared-react/src/components/options/columns/Columns.tsx new file mode 100644 index 0000000..939903f --- /dev/null +++ b/packages/grid-shared-react/src/components/options/columns/Columns.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 { ColumnsProps } from './columnProps'; + +export function Columns(_props: ColumnsProps) { + return null; +} + +Columns._GridReact = { + type: 'Grid_Option', + gridOption: 'columnDefaults', + role: 'columnsContainer' +}; 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..5d53aae --- /dev/null +++ b/packages/grid-shared-react/src/components/options/columns/columnProps.ts @@ -0,0 +1,80 @@ +/** + * Grid React integration. + * Copyright (c) 2025, Highsoft + * + * A valid license is required for using this software. + * See highcharts.com/license + * + */ + +import type { ReactNode } from 'react'; + +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 type ColumnsProps = ColumnOptionsProps & { + children?: ReactNode; +}; + +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) and applies the + * nested options from this element — header, cells, sorting, filtering, etc. + * + * 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 index da24ab3..933b32e 100644 --- a/packages/grid-shared-react/src/components/options/data/Data.tsx +++ b/packages/grid-shared-react/src/components/options/data/Data.tsx @@ -7,6 +7,8 @@ * */ +import type { ReactNode } from 'react'; + export type DataColumnValue = boolean | null | number | string | undefined; export type DataColumns = Record>; @@ -22,6 +24,10 @@ export interface DataProps { * Whether columns should be generated automatically from data source * column ids. * + * Defaults to `true`. When declarative `` children are used + * inside nested ``, the React wrapper sets this to `false` + * unless you pass this prop explicitly. + * * @default true */ autogenerateColumns?: boolean; @@ -47,6 +53,11 @@ export interface DataProps { * The column ID that contains the stable, unique row IDs. */ idColumn?: string; + /** + * Column configuration. Use `` with `` children to + * define which data fields are shown and how they are rendered. + */ + children?: ReactNode; } export function Data(_props: DataProps) { diff --git a/packages/grid-shared-react/src/components/options/index.ts b/packages/grid-shared-react/src/components/options/index.ts index 5f099f9..4a301f0 100644 --- a/packages/grid-shared-react/src/components/options/index.ts +++ b/packages/grid-shared-react/src/components/options/index.ts @@ -11,3 +11,13 @@ export { Caption } from './caption'; export type { CaptionProps } from './caption'; export { Data } from './data/Data'; export type { DataProps, DataColumns, DataColumnValue } from './data/Data'; +export { Columns } from './columns/Columns'; +export { Column } from './columns/Column'; +export type { + ColumnsProps, + ColumnProps, + ColumnOptionsProps, + ColumnDataType, + ColumnSortingOrder, + CellValueGetterContext +} from './columns/columnProps'; diff --git a/packages/grid-shared-react/src/hooks/useGrid.ts b/packages/grid-shared-react/src/hooks/useGrid.ts index adf07cf..c5b752f 100644 --- a/packages/grid-shared-react/src/hooks/useGrid.ts +++ b/packages/grid-shared-react/src/hooks/useGrid.ts @@ -14,7 +14,7 @@ import { useEffect, RefObject, useRef } from 'react'; */ export interface GridInstance { destroy(): void; - update(options: TOptions, redraw?: boolean): void; + update(options: TOptions, redraw?: boolean, oneToOne?: boolean): void; } /** @@ -89,7 +89,7 @@ export function useGrid({ // Apply any pending options that came in while we were initializing if (pendingOptionsRef.current !== void 0) { - grid.update(pendingOptionsRef.current, true); + grid.update(pendingOptionsRef.current, true, true); pendingOptionsRef.current = void 0; } @@ -122,8 +122,8 @@ export function useGrid({ } 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 ad49409..bcdd026 100644 --- a/packages/grid-shared-react/src/index.ts +++ b/packages/grid-shared-react/src/index.ts @@ -12,7 +12,18 @@ import { GridType, GridInstance } from './hooks/useGrid'; import type { GridProps, GridRefHandle } from './components/BaseGrid'; export { BaseGrid }; -export { Caption, Data } from './components/options'; +export { Caption, Data, Columns, Column } from './components/options'; export { getChildProps } from './utils/getChildProps'; -export type { CaptionProps, DataProps, DataColumns, DataColumnValue } from './components/options'; +export type { + CaptionProps, + DataProps, + DataColumns, + DataColumnValue, + ColumnsProps, + ColumnProps, + ColumnOptionsProps, + ColumnDataType, + ColumnSortingOrder, + CellValueGetterContext +} 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 index 3d11b1a..029dc08 100644 --- a/packages/grid-shared-react/src/utils/getChildProps.ts +++ b/packages/grid-shared-react/src/utils/getChildProps.ts @@ -7,8 +7,9 @@ * */ -import { isValidElement, ReactElement, ReactNode } from 'react'; +import { Fragment, isValidElement, ReactElement, ReactNode } from 'react'; import type { BaseGridOptionsComponent, BaseGridOptions } from '../components/BaseGridOptions'; +import { normalizeColumnOptions } from './mappers/columnOptions'; function objInsert( obj: Record, @@ -75,6 +76,22 @@ function renderChildren(children: ReactNode): string { return ''; } +function flattenChildren(childNodes: ReactNode): ReactNode[] { + if (childNodes == null || childNodes === false) { + return []; + } + + if (Array.isArray(childNodes)) { + return childNodes.flatMap((child) => flattenChildren(child)); + } + + if (isReactElement(childNodes) && childNodes.type === Fragment) { + return flattenChildren((childNodes.props as { children?: ReactNode }).children); + } + + return [childNodes]; +} + function getEffectiveMeta( component: BaseGridOptionsComponent, parentMeta?: BaseGridOptions @@ -96,12 +113,49 @@ function getEffectiveMeta( }; } +function parseColumnElement(child: ReactElement): Record { + const { children: _ignored, columnId, id: _cssId, ...props } = getChildPropsFromElement(child); + const options = normalizeColumnOptions(props); + + // columnId selects the column; Core expects the same value as `id`. + if (columnId !== void 0) { + options.id = columnId; + } + + return options; +} + +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 = (Array.isArray(children) ? children.flat() : [children]) + const resolvedChildren = flattenChildren(children) .map((child) => resolveOptionChild(child)) .filter((child): child is ReactElement => child !== null); + function handleDataChildren(childNodes: ReactNode): void { + for (const node of flattenChildren(childNodes)) { + if (!isReactElement(node)) { + continue; + } + + const role = getOptionComponent(node.type)?._GridReact.role; + + if (role === 'columnsContainer' || role === 'column') { + handleChild(node); + } + } + } + function handleChildren( childNodes: ReactNode, obj: Record, @@ -146,7 +200,36 @@ export function getChildProps(children: ReactNode): Record { const meta = getEffectiveMeta(component, parentMeta); - if (!meta.gridOption) { + if (!meta.gridOption && meta.role !== 'columnsContainer') { + return; + } + + const childProps = getChildPropsFromElement(child); + const { children: childChildren, ...props } = childProps; + + if (meta.role === 'columnsContainer') { + optionsFromChildren.columnDefaults = normalizeColumnOptions(props); + + for (const node of flattenChildren(childChildren as ReactNode)) { + if (isReactElement(node) && getOptionComponent(node.type)?._GridReact.role === 'column') { + pushColumn(optionsFromChildren, node); + } + } + return; + } + + if (meta.role === 'column') { + pushColumn(optionsFromChildren, child); + return; + } + + if (meta.gridOption === 'data') { + const dataOptions = (optionsFromChildren.data ?? ( + optionsFromChildren.data = {} + )) as Record; + + Object.assign(dataOptions, props); + handleDataChildren(childChildren as ReactNode); return; } @@ -155,8 +238,6 @@ export function getChildProps(children: ReactNode): Record { ); 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); @@ -181,9 +262,34 @@ export function getChildProps(children: ReactNode): Record { 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 isOptionElement(child: ReactElement): boolean { return getOptionComponent(child.type) !== null; } diff --git a/packages/grid-shared-react/src/utils/mappers/columnOptions.ts b/packages/grid-shared-react/src/utils/mappers/columnOptions.ts new file mode 100644 index 0000000..300c39c --- /dev/null +++ b/packages/grid-shared-react/src/utils/mappers/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/mapPrefixedProps.ts b/packages/grid-shared-react/src/utils/mappers/mapPrefixedProps.ts new file mode 100644 index 0000000..9fdd271 --- /dev/null +++ b/packages/grid-shared-react/src/utils/mappers/mapPrefixedProps.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 + * + */ + +/** + * 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); +} From fa3e1fafed8f80d6975a38e61bbc2b4a95f81442 Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Thu, 2 Jul 2026 13:57:02 +0200 Subject: [PATCH 09/28] Added ColumnDefaults compomnent. --- .../options/columns/ColumnDefaults.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 packages/grid-shared-react/src/components/options/columns/ColumnDefaults.tsx 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..cf2ce37 --- /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 { ColumnDefaultsProps } from './columnProps'; + +export function ColumnDefaults(_props: ColumnDefaultsProps) { + return null; +} + +ColumnDefaults._GridReact = { + type: 'Grid_Option', + gridOption: 'columnDefaults' +}; From c4e962d45eab44a8e7921944bebb70e95baadd51 Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Mon, 6 Jul 2026 11:45:53 +0200 Subject: [PATCH 10/28] Refactored Columsn and Data options. --- .../grid-lite/components-react/src/App.tsx | 142 +++++++++--------- packages/grid-lite-react/src/index.ts | 3 +- packages/grid-pro-react/src/index.ts | 3 +- .../src/components/BaseGridOptions.ts | 4 - .../src/components/options/columns/Column.tsx | 1 - .../options/columns/ColumnDefaults.tsx | 4 +- .../components/options/columns/Columns.tsx | 20 --- .../components/options/columns/columnProps.ts | 10 +- .../src/components/options/data/Data.tsx | 13 +- .../src/components/options/index.ts | 3 +- packages/grid-shared-react/src/index.ts | 3 +- .../src/utils/getChildProps.ts | 36 +---- 12 files changed, 86 insertions(+), 156 deletions(-) delete mode 100644 packages/grid-shared-react/src/components/options/columns/Columns.tsx diff --git a/examples/grid-lite/components-react/src/App.tsx b/examples/grid-lite/components-react/src/App.tsx index b0d8c05..eb439eb 100644 --- a/examples/grid-lite/components-react/src/App.tsx +++ b/examples/grid-lite/components-react/src/App.tsx @@ -7,7 +7,11 @@ import { Caption, Data, DataTable, +<<<<<<< HEAD Columns, +======= + ColumnDefaults, +>>>>>>> 868635b (Refactored Columsn and Data options.) Column, Description } from '@highcharts/grid-lite-react'; @@ -37,14 +41,14 @@ function App() { }); // 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] - } - }); + // 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 = () => { @@ -68,71 +72,69 @@ function App() { // gridRef={grid} callback={onGridCallback} > - Grid Caption - - - - - - - - + /> + + Grid Caption v2.1 + + + + + Grid Description {/* Grid Pagination */} diff --git a/packages/grid-lite-react/src/index.ts b/packages/grid-lite-react/src/index.ts index 906e41f..4272e3d 100644 --- a/packages/grid-lite-react/src/index.ts +++ b/packages/grid-lite-react/src/index.ts @@ -11,7 +11,7 @@ import GridLite from '@highcharts/grid-lite'; export { default as Grid } from './Grid'; export { default as GridLite } from './Grid'; -export { Caption, Data, Columns, Column, Description } from '@highcharts/grid-shared-react'; +export { Caption, Data, ColumnDefaults, Column, Description } 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 { @@ -22,7 +22,6 @@ export type { DataProps, DataColumns, DataColumnValue, - ColumnsProps, ColumnProps, ColumnOptionsProps, ColumnDataType, diff --git a/packages/grid-pro-react/src/index.ts b/packages/grid-pro-react/src/index.ts index 8aa80fc..a8a0200 100644 --- a/packages/grid-pro-react/src/index.ts +++ b/packages/grid-pro-react/src/index.ts @@ -11,7 +11,7 @@ import GridPro from '@highcharts/grid-pro'; export { default as Grid } from './Grid'; export { default as GridPro } from './Grid'; -export { Caption, Data, Columns, Column, Description } from '@highcharts/grid-shared-react'; +export { Caption, Data, ColumnDefaults, Column, Description } from '@highcharts/grid-shared-react'; export { DataTable, DataConnector } from '@highcharts/grid-pro'; export { merge } from '@highcharts/grid-pro/es-modules/Shared/Utilities.js'; export type { @@ -22,7 +22,6 @@ export type { DataProps, DataColumns, DataColumnValue, - ColumnsProps, ColumnProps, ColumnOptionsProps, ColumnDataType, diff --git a/packages/grid-shared-react/src/components/BaseGridOptions.ts b/packages/grid-shared-react/src/components/BaseGridOptions.ts index d35d54c..9440edc 100644 --- a/packages/grid-shared-react/src/components/BaseGridOptions.ts +++ b/packages/grid-shared-react/src/components/BaseGridOptions.ts @@ -22,10 +22,6 @@ export interface BaseGridOptions { childOption?: string; defaultOptions?: Record; isArrayType?: boolean; - /** - * Special parsing role for column-related components. - */ - role?: 'columnsContainer' | 'column'; } /** diff --git a/packages/grid-shared-react/src/components/options/columns/Column.tsx b/packages/grid-shared-react/src/components/options/columns/Column.tsx index f6c1d60..f5cd44a 100644 --- a/packages/grid-shared-react/src/components/options/columns/Column.tsx +++ b/packages/grid-shared-react/src/components/options/columns/Column.tsx @@ -16,6 +16,5 @@ export function Column(_props: ColumnProps) { Column._GridReact = { type: 'Grid_Option', gridOption: 'columns', - role: 'column', 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 index cf2ce37..d8569ba 100644 --- a/packages/grid-shared-react/src/components/options/columns/ColumnDefaults.tsx +++ b/packages/grid-shared-react/src/components/options/columns/ColumnDefaults.tsx @@ -7,9 +7,9 @@ * */ -import type { ColumnDefaultsProps } from './columnProps'; +import type { ColumnOptionsProps } from './columnProps'; -export function ColumnDefaults(_props: ColumnDefaultsProps) { +export function ColumnDefaults(_props: ColumnOptionsProps) { return null; } diff --git a/packages/grid-shared-react/src/components/options/columns/Columns.tsx b/packages/grid-shared-react/src/components/options/columns/Columns.tsx deleted file mode 100644 index 939903f..0000000 --- a/packages/grid-shared-react/src/components/options/columns/Columns.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Grid React integration. - * Copyright (c) 2025, Highsoft - * - * A valid license is required for using this software. - * See highcharts.com/license - * - */ - -import type { ColumnsProps } from './columnProps'; - -export function Columns(_props: ColumnsProps) { - return null; -} - -Columns._GridReact = { - type: 'Grid_Option', - gridOption: 'columnDefaults', - role: 'columnsContainer' -}; diff --git a/packages/grid-shared-react/src/components/options/columns/columnProps.ts b/packages/grid-shared-react/src/components/options/columns/columnProps.ts index 5d53aae..1452f99 100644 --- a/packages/grid-shared-react/src/components/options/columns/columnProps.ts +++ b/packages/grid-shared-react/src/components/options/columns/columnProps.ts @@ -7,8 +7,6 @@ * */ -import type { ReactNode } from 'react'; - export type ColumnDataType = 'string' | 'number' | 'boolean' | 'datetime'; export type ColumnSortingOrder = 'asc' | 'desc' | null; @@ -59,18 +57,14 @@ export interface ColumnOptionsProps { exportable?: boolean; } -export type ColumnsProps = ColumnOptionsProps & { - children?: ReactNode; -}; - 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) and applies the - * nested options from this element — header, cells, sorting, filtering, etc. + * 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). */ diff --git a/packages/grid-shared-react/src/components/options/data/Data.tsx b/packages/grid-shared-react/src/components/options/data/Data.tsx index 933b32e..7ec764a 100644 --- a/packages/grid-shared-react/src/components/options/data/Data.tsx +++ b/packages/grid-shared-react/src/components/options/data/Data.tsx @@ -7,8 +7,6 @@ * */ -import type { ReactNode } from 'react'; - export type DataColumnValue = boolean | null | number | string | undefined; export type DataColumns = Record>; @@ -24,9 +22,9 @@ export interface DataProps { * Whether columns should be generated automatically from data source * column ids. * - * Defaults to `true`. When declarative `` children are used - * inside nested ``, the React wrapper sets this to `false` - * unless you pass this prop explicitly. + * Defaults to `true`. When declarative `` components are used, + * the React wrapper sets this to `false` unless you pass this prop + * explicitly. * * @default true */ @@ -53,11 +51,6 @@ export interface DataProps { * The column ID that contains the stable, unique row IDs. */ idColumn?: string; - /** - * Column configuration. Use `` with `` children to - * define which data fields are shown and how they are rendered. - */ - children?: ReactNode; } export function Data(_props: DataProps) { diff --git a/packages/grid-shared-react/src/components/options/index.ts b/packages/grid-shared-react/src/components/options/index.ts index d90c37f..dbbf7f4 100644 --- a/packages/grid-shared-react/src/components/options/index.ts +++ b/packages/grid-shared-react/src/components/options/index.ts @@ -11,10 +11,9 @@ export { Caption } from './caption'; export type { CaptionProps } from './caption'; export { Data } from './data/Data'; export type { DataProps, DataColumns, DataColumnValue } from './data/Data'; -export { Columns } from './columns/Columns'; +export { ColumnDefaults } from './columns/ColumnDefaults'; export { Column } from './columns/Column'; export type { - ColumnsProps, ColumnProps, ColumnOptionsProps, ColumnDataType, diff --git a/packages/grid-shared-react/src/index.ts b/packages/grid-shared-react/src/index.ts index 6703780..3814da0 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, Data, Columns, Column, Description } from './components/options'; +export { Caption, Data, ColumnDefaults, Column, Description } from './components/options'; export { getChildProps } from './utils/getChildProps'; export type { CaptionProps, @@ -20,7 +20,6 @@ export type { DataProps, DataColumns, DataColumnValue, - ColumnsProps, ColumnProps, ColumnOptionsProps, ColumnDataType, diff --git a/packages/grid-shared-react/src/utils/getChildProps.ts b/packages/grid-shared-react/src/utils/getChildProps.ts index 029dc08..572376d 100644 --- a/packages/grid-shared-react/src/utils/getChildProps.ts +++ b/packages/grid-shared-react/src/utils/getChildProps.ts @@ -142,20 +142,6 @@ export function getChildProps(children: ReactNode): Record { .map((child) => resolveOptionChild(child)) .filter((child): child is ReactElement => child !== null); - function handleDataChildren(childNodes: ReactNode): void { - for (const node of flattenChildren(childNodes)) { - if (!isReactElement(node)) { - continue; - } - - const role = getOptionComponent(node.type)?._GridReact.role; - - if (role === 'columnsContainer' || role === 'column') { - handleChild(node); - } - } - } - function handleChildren( childNodes: ReactNode, obj: Record, @@ -200,39 +186,23 @@ export function getChildProps(children: ReactNode): Record { const meta = getEffectiveMeta(component, parentMeta); - if (!meta.gridOption && meta.role !== 'columnsContainer') { + if (!meta.gridOption) { return; } const childProps = getChildPropsFromElement(child); const { children: childChildren, ...props } = childProps; - if (meta.role === 'columnsContainer') { + if (meta.gridOption === 'columnDefaults') { optionsFromChildren.columnDefaults = normalizeColumnOptions(props); - - for (const node of flattenChildren(childChildren as ReactNode)) { - if (isReactElement(node) && getOptionComponent(node.type)?._GridReact.role === 'column') { - pushColumn(optionsFromChildren, node); - } - } return; } - if (meta.role === 'column') { + if (meta.gridOption === 'columns') { pushColumn(optionsFromChildren, child); return; } - if (meta.gridOption === 'data') { - const dataOptions = (optionsFromChildren.data ?? ( - optionsFromChildren.data = {} - )) as Record; - - Object.assign(dataOptions, props); - handleDataChildren(childChildren as ReactNode); - return; - } - const optionParent = optionsFromChildren[meta.gridOption] ?? ( optionsFromChildren[meta.gridOption] = meta.isArrayType ? [] : {} ); From 18ff1791167c515564325b090e18362677ae6d10 Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Mon, 6 Jul 2026 11:49:24 +0200 Subject: [PATCH 11/28] Fixed conflicts. --- examples/grid-lite/components-react/src/App.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/examples/grid-lite/components-react/src/App.tsx b/examples/grid-lite/components-react/src/App.tsx index eb439eb..ac64cd3 100644 --- a/examples/grid-lite/components-react/src/App.tsx +++ b/examples/grid-lite/components-react/src/App.tsx @@ -7,11 +7,7 @@ import { Caption, Data, DataTable, -<<<<<<< HEAD - Columns, -======= ColumnDefaults, ->>>>>>> 868635b (Refactored Columsn and Data options.) Column, Description } from '@highcharts/grid-lite-react'; From dc2402bbf22f3e27a8c801acac8ded8a88e972e1 Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Mon, 6 Jul 2026 12:57:08 +0200 Subject: [PATCH 12/28] Added basic pagination component. --- .../grid-lite/components-react/src/App.tsx | 29 ++++++- .../grid-lite/components-react/src/index.css | 7 ++ packages/grid-lite-react/src/index.ts | 5 +- packages/grid-pro-react/src/index.ts | 5 +- .../src/components/options/index.ts | 2 + .../options/pagination/Pagination.tsx | 21 +++++ .../components/options/pagination/index.ts | 11 +++ .../options/pagination/paginationProps.ts | 57 +++++++++++++ packages/grid-shared-react/src/index.ts | 5 +- .../src/utils/getChildProps.test.tsx | 50 ----------- .../src/utils/getChildProps.ts | 8 +- .../mappers/{ => column}/columnOptions.ts | 2 +- .../src/utils/mappers/column/index.ts | 10 +++ .../src/utils/mappers/pagination/index.ts | 10 +++ .../mappers/pagination/paginationOptions.ts | 84 +++++++++++++++++++ 15 files changed, 245 insertions(+), 61 deletions(-) create mode 100644 packages/grid-shared-react/src/components/options/pagination/Pagination.tsx create mode 100644 packages/grid-shared-react/src/components/options/pagination/index.ts create mode 100644 packages/grid-shared-react/src/components/options/pagination/paginationProps.ts delete mode 100644 packages/grid-shared-react/src/utils/getChildProps.test.tsx rename packages/grid-shared-react/src/utils/mappers/{ => column}/columnOptions.ts (90%) create mode 100644 packages/grid-shared-react/src/utils/mappers/column/index.ts create mode 100644 packages/grid-shared-react/src/utils/mappers/pagination/index.ts create mode 100644 packages/grid-shared-react/src/utils/mappers/pagination/paginationOptions.ts diff --git a/examples/grid-lite/components-react/src/App.tsx b/examples/grid-lite/components-react/src/App.tsx index ac64cd3..caa4be7 100644 --- a/examples/grid-lite/components-react/src/App.tsx +++ b/examples/grid-lite/components-react/src/App.tsx @@ -9,7 +9,8 @@ import { DataTable, ColumnDefaults, Column, - Description + Description, + Pagination } from '@highcharts/grid-lite-react'; function App() { @@ -61,6 +62,13 @@ function App() { console.info('(callback) grid:', grid); }; + // Pagination + // const [paginationEnabled, setPaginationEnabled] = useState(false); + + // const onPaginationClick = () => { + // setPaginationEnabled(true); + // }; + return ( <> Grid Description - {/* Grid Pagination */} + - +
+ + {/* */} +
); } diff --git a/examples/grid-lite/components-react/src/index.css b/examples/grid-lite/components-react/src/index.css index 04bacd8..9a63579 100644 --- a/examples/grid-lite/components-react/src/index.css +++ b/examples/grid-lite/components-react/src/index.css @@ -18,9 +18,16 @@ body { padding: 20px; } +#controls { + margin-top: 20px; + display: flex; + gap: 10px; +} + @media (prefers-color-scheme: dark) { body { background-color: #121212; color: #ffffff; } } + diff --git a/packages/grid-lite-react/src/index.ts b/packages/grid-lite-react/src/index.ts index 4272e3d..2929529 100644 --- a/packages/grid-lite-react/src/index.ts +++ b/packages/grid-lite-react/src/index.ts @@ -11,7 +11,7 @@ import GridLite from '@highcharts/grid-lite'; export { default as Grid } from './Grid'; export { default as GridLite } from './Grid'; -export { Caption, Data, ColumnDefaults, Column, Description } from '@highcharts/grid-shared-react'; +export { Caption, Data, ColumnDefaults, Column, Description, Pagination } 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 { @@ -26,6 +26,7 @@ export type { ColumnOptionsProps, ColumnDataType, ColumnSortingOrder, - CellValueGetterContext + CellValueGetterContext, + PaginationProps } 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 a8a0200..9eaacc3 100644 --- a/packages/grid-pro-react/src/index.ts +++ b/packages/grid-pro-react/src/index.ts @@ -11,7 +11,7 @@ import GridPro from '@highcharts/grid-pro'; export { default as Grid } from './Grid'; export { default as GridPro } from './Grid'; -export { Caption, Data, ColumnDefaults, Column, Description } from '@highcharts/grid-shared-react'; +export { Caption, Data, ColumnDefaults, Column, Description, Pagination } from '@highcharts/grid-shared-react'; export { DataTable, DataConnector } from '@highcharts/grid-pro'; export { merge } from '@highcharts/grid-pro/es-modules/Shared/Utilities.js'; export type { @@ -26,6 +26,7 @@ export type { ColumnOptionsProps, ColumnDataType, ColumnSortingOrder, - CellValueGetterContext + CellValueGetterContext, + PaginationProps } from '@highcharts/grid-shared-react'; export type GridOptions = GridPro.Options; diff --git a/packages/grid-shared-react/src/components/options/index.ts b/packages/grid-shared-react/src/components/options/index.ts index dbbf7f4..67c6419 100644 --- a/packages/grid-shared-react/src/components/options/index.ts +++ b/packages/grid-shared-react/src/components/options/index.ts @@ -22,3 +22,5 @@ export type { } from './columns/columnProps'; export { Description } from './description'; export type { DescriptionProps } from './description'; +export { Pagination } from './pagination'; +export type { PaginationProps } from './pagination'; 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..f2c9ad9 --- /dev/null +++ b/packages/grid-shared-react/src/components/options/pagination/Pagination.tsx @@ -0,0 +1,21 @@ +/** + * 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 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..a225c83 --- /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 './Pagination'; 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/index.ts b/packages/grid-shared-react/src/index.ts index 3814da0..989215c 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, Data, ColumnDefaults, Column, Description } from './components/options'; +export { Caption, Data, ColumnDefaults, Column, Description, Pagination } from './components/options'; export { getChildProps } from './utils/getChildProps'; export type { CaptionProps, @@ -24,6 +24,7 @@ export type { ColumnOptionsProps, ColumnDataType, ColumnSortingOrder, - CellValueGetterContext + CellValueGetterContext, + PaginationProps } from './components/options'; export type { GridType, GridInstance, GridProps, GridRefHandle }; diff --git a/packages/grid-shared-react/src/utils/getChildProps.test.tsx b/packages/grid-shared-react/src/utils/getChildProps.test.tsx deleted file mode 100644 index 1498a4e..0000000 --- a/packages/grid-shared-react/src/utils/getChildProps.test.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { Data } from '../components/options/data/Data'; -import { getChildProps } from './getChildProps'; - -describe('getChildProps', () => { - it('maps Data columns to options.data.columns', () => { - const columns = { - name: ['Alice', 'Bob'], - age: [23, 34] - }; - - expect(getChildProps()).toEqual({ - data: { - columns - } - }); - }); - - it('maps all Data props to options.data', () => { - const columns = { - name: ['Alice'] - }; - const connector = { id: 'csv' }; - const dataTable = { id: 'table-1' }; - - expect( - getChildProps( - - ) - ).toEqual({ - data: { - providerType: 'local', - autogenerateColumns: false, - columns, - connector, - dataTable, - updateOnChange: true, - idColumn: 'id' - } - }); - }); -}); diff --git a/packages/grid-shared-react/src/utils/getChildProps.ts b/packages/grid-shared-react/src/utils/getChildProps.ts index 572376d..a33c900 100644 --- a/packages/grid-shared-react/src/utils/getChildProps.ts +++ b/packages/grid-shared-react/src/utils/getChildProps.ts @@ -9,7 +9,8 @@ import { Fragment, isValidElement, ReactElement, ReactNode } from 'react'; import type { BaseGridOptionsComponent, BaseGridOptions } from '../components/BaseGridOptions'; -import { normalizeColumnOptions } from './mappers/columnOptions'; +import { normalizeColumnOptions } from './mappers/column'; +import { normalizePaginationOptions } from './mappers/pagination'; function objInsert( obj: Record, @@ -203,6 +204,11 @@ export function getChildProps(children: ReactNode): Record { return; } + if (meta.gridOption === 'pagination') { + optionsFromChildren.pagination = normalizePaginationOptions(props); + return; + } + const optionParent = optionsFromChildren[meta.gridOption] ?? ( optionsFromChildren[meta.gridOption] = meta.isArrayType ? [] : {} ); diff --git a/packages/grid-shared-react/src/utils/mappers/columnOptions.ts b/packages/grid-shared-react/src/utils/mappers/column/columnOptions.ts similarity index 90% rename from packages/grid-shared-react/src/utils/mappers/columnOptions.ts rename to packages/grid-shared-react/src/utils/mappers/column/columnOptions.ts index 300c39c..f1ae833 100644 --- a/packages/grid-shared-react/src/utils/mappers/columnOptions.ts +++ b/packages/grid-shared-react/src/utils/mappers/column/columnOptions.ts @@ -7,7 +7,7 @@ * */ -import { mapPrefixedProps } from './mapPrefixedProps'; +import { mapPrefixedProps } from '../mapPrefixedProps'; /** Flat prop prefix → nested Grid option key for columns. */ const 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/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..0db2d55 --- /dev/null +++ b/packages/grid-shared-react/src/utils/mappers/pagination/paginationOptions.ts @@ -0,0 +1,84 @@ +/** + * Grid React integration. + * Copyright (c) 2025, Highsoft + * + * A valid license is required for using this software. + * See highcharts.com/license + * + */ + +import type { PaginationProps } from '../../../components/options/pagination/paginationProps'; + +export function normalizePaginationOptions( + props: Record +): Record { + const { + pageInfo, + pageSizeSelector, + pageSizeOptions, + pageButtons, + pageButtonsCount, + firstLast, + previousNext, + enabled, + page, + pageSize, + align + } = props as PaginationProps; + + 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; +} From 5841659398105250603a0e745d8d9716819fa34c Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Mon, 6 Jul 2026 13:06:13 +0200 Subject: [PATCH 13/28] Cleaned up. --- .../src/components/options/pagination/Pagination.tsx | 2 -- .../src/components/options/pagination/index.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/grid-shared-react/src/components/options/pagination/Pagination.tsx b/packages/grid-shared-react/src/components/options/pagination/Pagination.tsx index f2c9ad9..8a169d7 100644 --- a/packages/grid-shared-react/src/components/options/pagination/Pagination.tsx +++ b/packages/grid-shared-react/src/components/options/pagination/Pagination.tsx @@ -9,8 +9,6 @@ import type { PaginationProps } from './paginationProps'; -export type { PaginationProps } from './paginationProps'; - export function Pagination(_props: PaginationProps) { return null; } diff --git a/packages/grid-shared-react/src/components/options/pagination/index.ts b/packages/grid-shared-react/src/components/options/pagination/index.ts index a225c83..d5d79cb 100644 --- a/packages/grid-shared-react/src/components/options/pagination/index.ts +++ b/packages/grid-shared-react/src/components/options/pagination/index.ts @@ -8,4 +8,4 @@ */ export { Pagination } from './Pagination'; -export type { PaginationProps } from './Pagination'; +export type { PaginationProps } from './paginationProps'; From c6ad53e8f570611a89c9bcf5e93fab1806f217e4 Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Mon, 6 Jul 2026 14:39:26 +0200 Subject: [PATCH 14/28] Added header component. --- .../grid-lite/components-react/src/App.tsx | 10 +++++- packages/grid-lite-react/src/index.ts | 15 ++++++-- packages/grid-pro-react/src/index.ts | 15 ++++++-- .../src/components/options/header/Header.tsx | 19 ++++++++++ .../components/options/header/headerProps.ts | 36 +++++++++++++++++++ .../src/components/options/header/index.ts | 15 ++++++++ .../src/components/options/index.ts | 6 ++++ packages/grid-shared-react/src/index.ts | 15 ++++++-- .../src/utils/getChildProps.ts | 9 +++++ 9 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 packages/grid-shared-react/src/components/options/header/Header.tsx create mode 100644 packages/grid-shared-react/src/components/options/header/headerProps.ts create mode 100644 packages/grid-shared-react/src/components/options/header/index.ts diff --git a/examples/grid-lite/components-react/src/App.tsx b/examples/grid-lite/components-react/src/App.tsx index caa4be7..cdb84e1 100644 --- a/examples/grid-lite/components-react/src/App.tsx +++ b/examples/grid-lite/components-react/src/App.tsx @@ -10,7 +10,8 @@ import { ColumnDefaults, Column, Description, - Pagination + Pagination, + Header } from '@highcharts/grid-lite-react'; function App() { @@ -98,6 +99,13 @@ function App() { cellRowHeader={false} /> Grid Caption v2.1 +
; +} + +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 index 67c6419..36984b3 100644 --- a/packages/grid-shared-react/src/components/options/index.ts +++ b/packages/grid-shared-react/src/components/options/index.ts @@ -24,3 +24,9 @@ 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/index.ts b/packages/grid-shared-react/src/index.ts index 989215c..4cfc3ca 100644 --- a/packages/grid-shared-react/src/index.ts +++ b/packages/grid-shared-react/src/index.ts @@ -12,7 +12,15 @@ import { GridType, GridInstance } from './hooks/useGrid'; import type { GridProps, GridRefHandle } from './components/BaseGrid'; export { BaseGrid }; -export { Caption, Data, ColumnDefaults, Column, Description, Pagination } from './components/options'; +export { + Caption, + Data, + ColumnDefaults, + Column, + Description, + Pagination, + Header +} from './components/options'; export { getChildProps } from './utils/getChildProps'; export type { CaptionProps, @@ -25,6 +33,9 @@ export type { ColumnDataType, ColumnSortingOrder, CellValueGetterContext, - PaginationProps + 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 index a33c900..4564d86 100644 --- a/packages/grid-shared-react/src/utils/getChildProps.ts +++ b/packages/grid-shared-react/src/utils/getChildProps.ts @@ -209,6 +209,15 @@ export function getChildProps(children: ReactNode): Record { return; } + if (meta.gridOption === 'header') { + const { header, children: _ignored } = props; + + if (header !== void 0) { + optionsFromChildren.header = header; + } + return; + } + const optionParent = optionsFromChildren[meta.gridOption] ?? ( optionsFromChildren[meta.gridOption] = meta.isArrayType ? [] : {} ); From 605ce6c8a476b5e7a8cd73a527d0125aca671465 Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Wed, 8 Jul 2026 10:24:19 +0200 Subject: [PATCH 15/28] Added pagination position. --- .../grid-lite/components-react/src/App.tsx | 13 +++++++ .../src/utils/getChildProps.ts | 35 ++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/examples/grid-lite/components-react/src/App.tsx b/examples/grid-lite/components-react/src/App.tsx index cdb84e1..f24838d 100644 --- a/examples/grid-lite/components-react/src/App.tsx +++ b/examples/grid-lite/components-react/src/App.tsx @@ -77,6 +77,19 @@ function App() { // gridRef={grid} callback={onGridCallback} > + {/* */} { const resolvedChildren = flattenChildren(children) .map((child) => resolveOptionChild(child)) .filter((child): child is ReactElement => child !== null); + const firstNonPaginationIndex = getFirstNonPaginationIndex(resolvedChildren); function handleChildren( childNodes: ReactNode, @@ -205,7 +206,13 @@ export function getChildProps(children: ReactNode): Record { } if (meta.gridOption === 'pagination') { - optionsFromChildren.pagination = normalizePaginationOptions(props); + const pagination = normalizePaginationOptions(props); + pagination.position = isTopPaginationChild( + child, + resolvedChildren, + firstNonPaginationIndex + ) ? 'top' : 'bottom'; + optionsFromChildren.pagination = pagination; return; } @@ -275,6 +282,32 @@ function applyDeclarativeColumnDefaults( } } +function getFirstNonPaginationIndex(children: ReactElement[]): number { + return children.findIndex((child) => { + const component = getOptionComponent(child.type); + + return component?._GridReact.gridOption !== 'pagination'; + }); +} + +function isTopPaginationChild( + child: ReactElement, + children: ReactElement[], + firstNonPaginationIndex: number +): boolean { + const childIndex = children.indexOf(child); + + if (childIndex === -1) { + return false; + } + + if (firstNonPaginationIndex === -1) { + return true; + } + + return childIndex < firstNonPaginationIndex; +} + function isOptionElement(child: ReactElement): boolean { return getOptionComponent(child.type) !== null; } From 76f09c9a4db251923bf18d433055cb1c4fc92493 Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Wed, 8 Jul 2026 10:51:16 +0200 Subject: [PATCH 16/28] Optymized pagination position. --- .../src/utils/getChildProps.ts | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/packages/grid-shared-react/src/utils/getChildProps.ts b/packages/grid-shared-react/src/utils/getChildProps.ts index ea9712e..cc4880d 100644 --- a/packages/grid-shared-react/src/utils/getChildProps.ts +++ b/packages/grid-shared-react/src/utils/getChildProps.ts @@ -142,7 +142,6 @@ export function getChildProps(children: ReactNode): Record { const resolvedChildren = flattenChildren(children) .map((child) => resolveOptionChild(child)) .filter((child): child is ReactElement => child !== null); - const firstNonPaginationIndex = getFirstNonPaginationIndex(resolvedChildren); function handleChildren( childNodes: ReactNode, @@ -207,11 +206,9 @@ export function getChildProps(children: ReactNode): Record { if (meta.gridOption === 'pagination') { const pagination = normalizePaginationOptions(props); - pagination.position = isTopPaginationChild( - child, - resolvedChildren, - firstNonPaginationIndex - ) ? 'top' : 'bottom'; + pagination.position = isTopPaginationChild(child, resolvedChildren) ? + 'top' : + 'bottom'; optionsFromChildren.pagination = pagination; return; } @@ -282,18 +279,9 @@ function applyDeclarativeColumnDefaults( } } -function getFirstNonPaginationIndex(children: ReactElement[]): number { - return children.findIndex((child) => { - const component = getOptionComponent(child.type); - - return component?._GridReact.gridOption !== 'pagination'; - }); -} - function isTopPaginationChild( child: ReactElement, - children: ReactElement[], - firstNonPaginationIndex: number + children: ReactElement[] ): boolean { const childIndex = children.indexOf(child); @@ -301,11 +289,9 @@ function isTopPaginationChild( return false; } - if (firstNonPaginationIndex === -1) { - return true; - } - - return childIndex < firstNonPaginationIndex; + return children + .slice(0, childIndex) + .every((candidate) => getOptionComponent(candidate.type)?._GridReact.gridOption === 'pagination'); } function isOptionElement(child: ReactElement): boolean { From 6d16a7568bb13eab1a085d1f6b6598fff956e69c Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Wed, 8 Jul 2026 11:54:30 +0200 Subject: [PATCH 17/28] Added components tests. --- packages/grid-lite-react/tests/Grid.test.tsx | 23 +++ packages/grid-pro-react/tests/Grid.test.tsx | 23 +++ .../tests/createGridTests.tsx | 138 ++++++++++++++++++ .../tests/options/Data.test.tsx | 38 +++++ .../tests/options/Header.test.tsx | 13 ++ .../tests/options/Pagination.test.tsx | 63 ++++++++ 6 files changed, 298 insertions(+) create mode 100644 packages/grid-lite-react/tests/Grid.test.tsx create mode 100644 packages/grid-pro-react/tests/Grid.test.tsx create mode 100644 packages/grid-shared-react/tests/createGridTests.tsx create mode 100644 packages/grid-shared-react/tests/options/Data.test.tsx create mode 100644 packages/grid-shared-react/tests/options/Header.test.tsx create mode 100644 packages/grid-shared-react/tests/options/Pagination.test.tsx diff --git a/packages/grid-lite-react/tests/Grid.test.tsx b/packages/grid-lite-react/tests/Grid.test.tsx new file mode 100644 index 0000000..140e4eb --- /dev/null +++ b/packages/grid-lite-react/tests/Grid.test.tsx @@ -0,0 +1,23 @@ +import { createGridTests } from '@highcharts/grid-shared-react/tests/createGridTests'; +import { Grid, GridOptions } from '../src/index'; + +createGridTests( + 'Grid Lite', + Grid, + { + dataTable: { + columns: { + name: ['Alice', 'Bob'], + age: [30, 25] + } + } + }, + { + dataTable: { + columns: { + name: ['Charlie', 'Diana'], + age: [40, 35] + } + } + } +); diff --git a/packages/grid-pro-react/tests/Grid.test.tsx b/packages/grid-pro-react/tests/Grid.test.tsx new file mode 100644 index 0000000..65e49d7 --- /dev/null +++ b/packages/grid-pro-react/tests/Grid.test.tsx @@ -0,0 +1,23 @@ +import { createGridTests } from '@highcharts/grid-shared-react/tests/createGridTests'; +import { Grid, GridOptions } from '../src/index'; + +createGridTests( + 'Grid Pro', + Grid, + { + dataTable: { + columns: { + name: ['Alice', 'Bob'], + age: [30, 25] + } + } + }, + { + dataTable: { + columns: { + name: ['Charlie', 'Diana'], + age: [40, 35] + } + } + } +); diff --git a/packages/grid-shared-react/tests/createGridTests.tsx b/packages/grid-shared-react/tests/createGridTests.tsx new file mode 100644 index 0000000..acf05db --- /dev/null +++ b/packages/grid-shared-react/tests/createGridTests.tsx @@ -0,0 +1,138 @@ +import { render, waitFor, fireEvent } from '@testing-library/react'; +import { + useRef, + useState, + type ComponentType +} from 'react'; +import { describe, it, expect, vi } from 'vitest'; +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. + */ +export function createGridTests( + name: string, + GridComponent: ComponentType>, + testOptions: TOptions, + updatedOptions: TOptions +) { + + describe(name, () => { + it('renders a container div and initializes grid', async () => { + let gridInstance: GridInstance | null = null; + + const onGridReady = (grid: GridInstance) => { + gridInstance = grid; + }; + + const { container } = render( + + ); + + expect(container.firstChild).toBeInstanceOf(HTMLDivElement); + + await waitFor(() => { + expect(gridInstance).not.toBeNull(); + }); + }); + + it('provides grid instance via gridRef prop', async () => { + let gridRef: React.RefObject | null>; + let initialized = false; + + function TestComponent() { + gridRef = useRef>(null); + return ( + { initialized = true; }} + /> + ); + } + + render(); + + await waitFor(() => { + expect(initialized).toBe(true); + expect(gridRef.current?.grid).toBeDefined(); + }); + }); + + it('calls callback when grid is initialized', async () => { + const callback = vi.fn(); + render(); + + await waitFor(() => { + expect(callback).toHaveBeenCalled(); + }); + }); + + it('updates grid when options change', async () => { + let gridInstance: GridInstance | null = null; + + function TestComponent() { + const [opts, setOpts] = useState(testOptions); + + const onGridReady = (grid: GridInstance) => { + gridInstance = grid; + }; + + return ( + <> + + + + ); + } + + const { getByTestId, container } = render(); + + // Wait for initial grid creation + await waitFor(() => { + expect(gridInstance).not.toBeNull(); + }); + + // Trigger options change + fireEvent.click(getByTestId('update-options')); + + // Wait for the grid to update with new data + await waitFor(() => { + const cells = container.querySelectorAll('td[data-value]'); + const values = Array.from(cells).map(c => c.getAttribute('data-value')); + expect(values).toContain('Charlie'); + }); + }); + + it('calls destroy() on unmount', async () => { + let destroySpy: ReturnType | null = null; + + const onGridReady = (grid: GridInstance) => { + destroySpy = vi.spyOn(grid, 'destroy'); + }; + + const { unmount } = render( + + ); + + // Wait for grid to initialize + await waitFor(() => { + expect(destroySpy).not.toBeNull(); + }); + + // Unmount and verify destroy was called + unmount(); + + expect(destroySpy).toHaveBeenCalledTimes(1); + }); + + }); +} diff --git a/packages/grid-shared-react/tests/options/Data.test.tsx b/packages/grid-shared-react/tests/options/Data.test.tsx new file mode 100644 index 0000000..a6f165a --- /dev/null +++ b/packages/grid-shared-react/tests/options/Data.test.tsx @@ -0,0 +1,38 @@ +import { describe, it, expect } from 'vitest'; +import { Data } from '../../src/components/options/data/Data'; +import { getChildProps } from '../../src/utils/getChildProps'; + +describe('Data', () => { + it('maps columns to options.data.columns', () => { + const columns = { + product: ['Apples', 'Oranges'], + price: [1.2, 2.4] + }; + + expect( + getChildProps( + + ) + ).toEqual({ + data: { + columns, + providerType: 'local', + autogenerateColumns: true + } + }); + }); + + it('maps dataTable to options.data.dataTable', () => { + const dataTable = { id: 'table-1', rows: [] }; + + expect(getChildProps()).toEqual({ + data: { + dataTable + } + }); + }); +}); diff --git a/packages/grid-shared-react/tests/options/Header.test.tsx b/packages/grid-shared-react/tests/options/Header.test.tsx new file mode 100644 index 0000000..58fa656 --- /dev/null +++ b/packages/grid-shared-react/tests/options/Header.test.tsx @@ -0,0 +1,13 @@ +import { describe, it, expect } from 'vitest'; +import { Header } from '../../src/components/options/header/Header'; +import { getChildProps } from '../../src/utils/getChildProps'; + +describe('Header', () => { + it('maps header prop to options.header', () => { + const header = ['product', { columnId: 'price', format: '{value} USD' }]; + + expect(getChildProps(
)).toEqual({ + header + }); + }); +}); diff --git a/packages/grid-shared-react/tests/options/Pagination.test.tsx b/packages/grid-shared-react/tests/options/Pagination.test.tsx new file mode 100644 index 0000000..1608d8d --- /dev/null +++ b/packages/grid-shared-react/tests/options/Pagination.test.tsx @@ -0,0 +1,63 @@ +import { describe, it, expect } from 'vitest'; +import { Data } from '../../src/components/options/data/Data'; +import { Pagination } from '../../src/components/options/pagination/Pagination'; +import { getChildProps } from '../../src/utils/getChildProps'; + +describe('Pagination', () => { + it('normalizes pagination props into options.pagination', () => { + expect( + getChildProps( + + ) + ).toEqual({ + pagination: { + enabled: false, + page: 2, + pageSize: 25, + align: 'center', + position: 'top', + controls: { + pageInfo: true, + pageSizeSelector: { + enabled: true, + options: [10, 25, 50] + }, + pageButtons: { + enabled: true, + count: 5 + }, + firstLastButtons: true, + previousNextButtons: false + } + } + }); + }); + + it('sets position to top for the first pagination and bottom after other options', () => { + const top = getChildProps( + <> + + + + ); + const bottom = getChildProps( + <> + + + + ); + + expect(top.pagination).toMatchObject({ position: 'top' }); + expect(bottom.pagination).toMatchObject({ position: 'bottom' }); + }); +}); From 3796a7e671809de7fca60418e54079d18fbf5f87 Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Wed, 8 Jul 2026 12:30:24 +0200 Subject: [PATCH 18/28] Added linter to PR runner. --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c844038..cc0a7ed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,6 +26,9 @@ jobs: - name: Build packages run: pnpm build + - name: Run linter + run: pnpm lint + - name: Install Playwright browsers run: pnpm exec playwright install --with-deps chromium From aa121c873d307376b62c8d84fcfb9086c2cbe3ee Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Wed, 8 Jul 2026 13:08:31 +0200 Subject: [PATCH 19/28] Rephrased rules in linter. --- eslint.config.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/eslint.config.js b/eslint.config.js index 6be1776..d7605c1 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -24,7 +24,13 @@ export default defineConfig( '@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 + }] }, }, { From 0e1a9099ff47a032eac09c31db5fa46ce6b4aa31 Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Thu, 9 Jul 2026 10:08:57 +0200 Subject: [PATCH 20/28] Added husky precommit action. --- .github/workflows/test.yml | 27 +++++++++++++++++++++++---- .husky/pre-commit | 2 ++ vitest.config.ts | 5 ++++- 3 files changed, 29 insertions(+), 5 deletions(-) create mode 100755 .husky/pre-commit diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cc0a7ed..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 @@ -26,9 +48,6 @@ jobs: - name: Build packages run: pnpm build - - name: Run linter - run: pnpm lint - - name: Install Playwright browsers run: pnpm exec playwright install --with-deps chromium 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/vitest.config.ts b/vitest.config.ts index 8ad4fee..e23f4e9 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,7 +3,10 @@ import { playwright } from '@vitest/browser-playwright'; export default defineConfig({ test: { - include: ['packages/*/src/**/*.test.{ts,tsx}'], + include: [ + 'packages/*/src/**/*.test.{ts,tsx}', + 'packages/*/tests/**/*.test.{ts,tsx}' + ], globals: true, css: true, browser: { From 7b50a33103f9c477ed7f1a22657a586cf537db79 Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Thu, 9 Jul 2026 10:12:07 +0200 Subject: [PATCH 21/28] Linted. --- .../src/components/BaseGridOptions.ts | 6 +- .../src/test/createGridTests.tsx | 137 ------------------ .../src/utils/getChildProps.ts | 60 +++++--- .../src/utils/mappers/mapPrefixedProps.ts | 8 +- 4 files changed, 52 insertions(+), 159 deletions(-) delete mode 100644 packages/grid-shared-react/src/test/createGridTests.tsx diff --git a/packages/grid-shared-react/src/components/BaseGridOptions.ts b/packages/grid-shared-react/src/components/BaseGridOptions.ts index 9440edc..aabeb10 100644 --- a/packages/grid-shared-react/src/components/BaseGridOptions.ts +++ b/packages/grid-shared-react/src/components/BaseGridOptions.ts @@ -8,7 +8,8 @@ */ /** - * Metadata attached to declarative option components rendered as BaseGrid children. + * Metadata attached to declarative option components + * rendered as BaseGrid children. */ export interface BaseGridOptions { type: 'Grid_Option'; @@ -25,7 +26,8 @@ export interface BaseGridOptions { } /** - * A React component that maps JSX props to a Grid options path via `_GridReact`. + * 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/test/createGridTests.tsx b/packages/grid-shared-react/src/test/createGridTests.tsx deleted file mode 100644 index 1faed12..0000000 --- a/packages/grid-shared-react/src/test/createGridTests.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { render, waitFor, fireEvent } from '@testing-library/react'; -import { - useRef, - useState, - type ComponentType -} from 'react'; -import { describe, it, expect, vi } from 'vitest'; -import { GridProps, GridRefHandle } from '../components/BaseGrid'; -import { GridInstance } from '../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. - */ -export function createGridTests( - name: string, - GridComponent: ComponentType>, - testOptions: TOptions, - updatedOptions: TOptions -) { - - describe(name, () => { - it('renders a container div and initializes grid', async () => { - let gridInstance: GridInstance | null = null; - - const onGridReady = (grid: GridInstance) => { - gridInstance = grid; - }; - - const { container } = render( - - ); - - expect(container.firstChild).toBeInstanceOf(HTMLDivElement); - - await waitFor(() => { - expect(gridInstance).not.toBeNull(); - }); - }); - - it('provides grid instance via gridRef prop', async () => { - let gridRef: React.RefObject | null>; - let initialized = false; - - function TestComponent() { - gridRef = useRef>(null); - return ( - { initialized = true; }} - /> - ); - } - - render(); - - await waitFor(() => { - expect(initialized).toBe(true); - expect(gridRef.current?.grid).toBeDefined(); - }); - }); - - it('calls callback when grid is initialized', async () => { - const callback = vi.fn(); - render(); - - await waitFor(() => { - expect(callback).toHaveBeenCalled(); - }); - }); - - it('updates grid when options change', async () => { - let gridInstance: GridInstance | null = null; - - function TestComponent() { - const [opts, setOpts] = useState(testOptions); - - const onGridReady = (grid: GridInstance) => { - gridInstance = grid; - }; - - return ( - <> - - - - ); - } - - const { getByTestId, container } = render(); - - // Wait for initial grid creation - await waitFor(() => { - expect(gridInstance).not.toBeNull(); - }); - - // Trigger options change - fireEvent.click(getByTestId('update-options')); - - // Wait for the grid to update with new data - await waitFor(() => { - const cells = container.querySelectorAll('td[data-value]'); - const values = Array.from(cells).map(c => c.getAttribute('data-value')); - expect(values).toContain('Charlie'); - }); - }); - - it('calls destroy() on unmount', async () => { - let destroySpy: ReturnType | null = null; - - const onGridReady = (grid: GridInstance) => { - destroySpy = vi.spyOn(grid, 'destroy'); - }; - - const { unmount } = render( - - ); - - // Wait for grid to initialize - await waitFor(() => { - expect(destroySpy).not.toBeNull(); - }); - - // Unmount and verify destroy was called - unmount(); - - expect(destroySpy).toHaveBeenCalledTimes(1); - }); - - }); -} diff --git a/packages/grid-shared-react/src/utils/getChildProps.ts b/packages/grid-shared-react/src/utils/getChildProps.ts index cc4880d..bc1c5a6 100644 --- a/packages/grid-shared-react/src/utils/getChildProps.ts +++ b/packages/grid-shared-react/src/utils/getChildProps.ts @@ -59,7 +59,9 @@ function getOptionComponent(type: unknown): BaseGridOptionsComponent | null { return component._GridReact ? type as BaseGridOptionsComponent : null; } -function getChildPropsFromElement(child: ReactElement): Record { +function getChildPropsFromElement( + child: ReactElement +): Record { return (child.props ?? {}) as Record; } @@ -87,7 +89,8 @@ function flattenChildren(childNodes: ReactNode): ReactNode[] { } if (isReactElement(childNodes) && childNodes.type === Fragment) { - return flattenChildren((childNodes.props as { children?: ReactNode }).children); + const fragmentProps = childNodes.props as { children?: ReactNode }; + return flattenChildren(fragmentProps.children); } return [childNodes]; @@ -115,7 +118,15 @@ function getEffectiveMeta( } function parseColumnElement(child: ReactElement): Record { - const { children: _ignored, columnId, id: _cssId, ...props } = getChildPropsFromElement(child); + const { + children, + id, + columnId, + ...props + } = getChildPropsFromElement(child); + void children; + void id; + const options = normalizeColumnOptions(props); // columnId selects the column; Core expects the same value as `id`. @@ -178,7 +189,10 @@ export function getChildProps(children: ReactNode): Record { } } - function handleChild(child: ReactElement, parentMeta?: BaseGridOptions): void { + function handleChild( + child: ReactElement, + parentMeta?: BaseGridOptions + ): void { const component = getOptionComponent(child.type); if (!component) { @@ -206,18 +220,17 @@ export function getChildProps(children: ReactNode): Record { if (meta.gridOption === 'pagination') { const pagination = normalizePaginationOptions(props); - pagination.position = isTopPaginationChild(child, resolvedChildren) ? - 'top' : - 'bottom'; + pagination.position = isTopPaginationChild( + child, + resolvedChildren + ) ? 'top' : 'bottom'; optionsFromChildren.pagination = pagination; return; } if (meta.gridOption === 'header') { - const { header, children: _ignored } = props; - - if (header !== void 0) { - optionsFromChildren.header = header; + if (props.header !== void 0) { + optionsFromChildren.header = props.header; } return; } @@ -226,7 +239,9 @@ export function getChildProps(children: ReactNode): Record { optionsFromChildren[meta.gridOption] = meta.isArrayType ? [] : {} ); const parentIsArray = Array.isArray(optionParent); - const insertInto = parentIsArray ? {} : optionParent as Record; + const insertInto = parentIsArray + ? {} + : optionParent as Record; if (meta.defaultOptions) { Object.assign(insertInto, meta.defaultOptions); @@ -243,7 +258,10 @@ export function getChildProps(children: ReactNode): Record { } if (parentIsArray) { - (optionsFromChildren[meta.gridOption] as unknown[]).push(insertInto); + const optionItems = optionsFromChildren[ + meta.gridOption + ] as unknown[]; + optionItems.push(insertInto); } } @@ -258,7 +276,8 @@ export function getChildProps(children: ReactNode): Record { /** * When declarative `` components are present, only those columns - * should render unless `data.autogenerateColumns` is set explicitly on ``. + * should render unless `data.autogenerateColumns` is set + * explicitly on ``. */ function applyDeclarativeColumnDefaults( optionsFromChildren: Record @@ -291,7 +310,11 @@ function isTopPaginationChild( return children .slice(0, childIndex) - .every((candidate) => getOptionComponent(candidate.type)?._GridReact.gridOption === 'pagination'); + .every((candidate) => { + const gridOption = getOptionComponent(candidate.type) + ?._GridReact.gridOption; + return gridOption === 'pagination'; + }); } function isOptionElement(child: ReactElement): boolean { @@ -313,9 +336,10 @@ function resolveOptionChild(child: ReactNode): ReactElement | null { return null; } - const rendered = (child.type as (props: Record) => ReactNode)( - getChildPropsFromElement(child) - ); + const renderChild = child.type as ( + props: Record + ) => ReactNode; + const rendered = renderChild(getChildPropsFromElement(child)); if (isReactElement(rendered) && getOptionComponent(rendered.type)) { return rendered; diff --git a/packages/grid-shared-react/src/utils/mappers/mapPrefixedProps.ts b/packages/grid-shared-react/src/utils/mappers/mapPrefixedProps.ts index 9fdd271..82418e3 100644 --- a/packages/grid-shared-react/src/utils/mappers/mapPrefixedProps.ts +++ b/packages/grid-shared-react/src/utils/mappers/mapPrefixedProps.ts @@ -27,11 +27,15 @@ export function mapPrefixedProps( ): Record { const result = { ...props }; const groups: Record> = {}; - const prefixes = Object.keys(prefixToGroup).sort((a, b) => b.length - a.length); + 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 + (candidate) => ( + flatKey.startsWith(candidate) + && flatKey.length > candidate.length + ) ); if (!prefix) { From 108a85a5725f2b70d6110a167b80d4501496c6cd Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Thu, 9 Jul 2026 10:13:09 +0200 Subject: [PATCH 22/28] Linted useGrid hook. --- .../grid-shared-react/src/hooks/useGrid.ts | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/grid-shared-react/src/hooks/useGrid.ts b/packages/grid-shared-react/src/hooks/useGrid.ts index c5b752f..f5e1756 100644 --- a/packages/grid-shared-react/src/hooks/useGrid.ts +++ b/packages/grid-shared-react/src/hooks/useGrid.ts @@ -24,7 +24,11 @@ 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 { @@ -62,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 @@ -73,21 +78,24 @@ 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 = 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 + // 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; @@ -122,7 +130,8 @@ export function useGrid({ } if (currGridRef.current) { - // Declarative React options replace the previous snapshot (oneToOne). + // Declarative React options replace the previous + // snapshot (oneToOne). currGridRef.current.update(options, true, true); } else { // Grid still initializing, queue the update From bfc063a898f993f97ba7638d6a56386023a8aaa3 Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Thu, 9 Jul 2026 10:33:29 +0200 Subject: [PATCH 23/28] Fixed packages. --- package.json | 71 ++++++++++++++++++++++++++------------------------ pnpm-lock.yaml | 10 +++++++ 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 5d1eb21..0aaabc9 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": "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", + "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/pnpm-lock.yaml b/pnpm-lock.yaml index ff81d18..648ccc3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: globals: specifier: ^17.0.0 version: 17.0.0 + husky: + specifier: ^9.1.7 + version: 9.1.7 playwright: specifier: ^1.57.0 version: 1.57.0 @@ -1532,6 +1535,11 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -3282,6 +3290,8 @@ snapshots: - supports-color optional: true + husky@9.1.7: {} + ignore@5.3.2: {} ignore@7.0.5: {} From 9dd5e0e26932c813cd2dcbd325c7875c85d59ae0 Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Thu, 9 Jul 2026 10:37:26 +0200 Subject: [PATCH 24/28] Linted Grid. --- packages/grid-lite-react/src/Grid.tsx | 3 ++- packages/grid-pro-react/src/Grid.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/grid-lite-react/src/Grid.tsx b/packages/grid-lite-react/src/Grid.tsx index aa47803..44b5643 100644 --- a/packages/grid-lite-react/src/Grid.tsx +++ b/packages/grid-lite-react/src/Grid.tsx @@ -22,7 +22,8 @@ export default function GridLite(props: GridProps) { const { gridRef, children, options, ...gridProps } = props; const childOptions = useMemo(() => getChildProps(children), [children]); const columnKey = useMemo(() => { - const columns = childOptions.columns as Array<{ id?: string }> | undefined; + const columns = childOptions.columns as + Array<{ id?: string }> | undefined; return columns?.map((column) => column.id).join('\0') ?? ''; }, [childOptions]); diff --git a/packages/grid-pro-react/src/Grid.tsx b/packages/grid-pro-react/src/Grid.tsx index c236cfd..c9e596f 100644 --- a/packages/grid-pro-react/src/Grid.tsx +++ b/packages/grid-pro-react/src/Grid.tsx @@ -22,7 +22,8 @@ export default function GridPro(props: GridProps) { const { gridRef, children, options, ...gridProps } = props; const childOptions = useMemo(() => getChildProps(children), [children]); const columnKey = useMemo(() => { - const columns = childOptions.columns as Array<{ id?: string }> | undefined; + const columns = childOptions.columns as + Array<{ id?: string }> | undefined; return columns?.map((column) => column.id).join('\0') ?? ''; }, [childOptions]); From 2ade77e12270fb0df96370aac2ec5d219db196dc Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Thu, 9 Jul 2026 10:41:59 +0200 Subject: [PATCH 25/28] Cleaned up. --- .../src/__tests__/Grid.test.tsx | 23 ------------------- .../src/__tests__/Grid.test.tsx | 23 ------------------- 2 files changed, 46 deletions(-) delete mode 100644 packages/grid-lite-react/src/__tests__/Grid.test.tsx delete mode 100644 packages/grid-pro-react/src/__tests__/Grid.test.tsx diff --git a/packages/grid-lite-react/src/__tests__/Grid.test.tsx b/packages/grid-lite-react/src/__tests__/Grid.test.tsx deleted file mode 100644 index 770322a..0000000 --- a/packages/grid-lite-react/src/__tests__/Grid.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { createGridTests } from '@highcharts/grid-shared-react/src/test/createGridTests'; -import { Grid, GridOptions } from '../index'; - -createGridTests( - 'Grid Lite', - Grid, - { - dataTable: { - columns: { - name: ['Alice', 'Bob'], - age: [30, 25] - } - } - }, - { - dataTable: { - columns: { - name: ['Charlie', 'Diana'], - age: [40, 35] - } - } - } -); diff --git a/packages/grid-pro-react/src/__tests__/Grid.test.tsx b/packages/grid-pro-react/src/__tests__/Grid.test.tsx deleted file mode 100644 index e44d6d9..0000000 --- a/packages/grid-pro-react/src/__tests__/Grid.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { createGridTests } from '@highcharts/grid-shared-react/src/test/createGridTests'; -import { Grid, GridOptions } from '../index'; - -createGridTests( - 'Grid Pro', - Grid, - { - dataTable: { - columns: { - name: ['Alice', 'Bob'], - age: [30, 25] - } - } - }, - { - dataTable: { - columns: { - name: ['Charlie', 'Diana'], - age: [40, 35] - } - } - } -); From 8bcf49e4931bc8b0e1fbaab87da5f7c396a818ec Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Thu, 9 Jul 2026 12:13:11 +0200 Subject: [PATCH 26/28] Added Caption test. --- .../tests/options/Caption.test.tsx | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 packages/grid-shared-react/tests/options/Caption.test.tsx diff --git a/packages/grid-shared-react/tests/options/Caption.test.tsx b/packages/grid-shared-react/tests/options/Caption.test.tsx new file mode 100644 index 0000000..832944e --- /dev/null +++ b/packages/grid-shared-react/tests/options/Caption.test.tsx @@ -0,0 +1,21 @@ +import { describe, it, expect } from 'vitest'; +import { Caption } from '../../src/components/options/caption/Caption'; +import { getChildProps } from '../../src/utils/getChildProps'; + +describe('Caption', () => { + it('maps caption props and children into options.caption', () => { + expect( + getChildProps( + + Sales table + + ) + ).toEqual({ + caption: { + className: 'grid-caption', + htmlTag: 'h2', + text: 'Sales table' + } + }); + }); +}); From 41c7bf5629752f86a201695ed47c01193432f342 Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Thu, 9 Jul 2026 14:12:04 +0200 Subject: [PATCH 27/28] Added Description test. --- .../tests/options/Description.test.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 packages/grid-shared-react/tests/options/Description.test.tsx diff --git a/packages/grid-shared-react/tests/options/Description.test.tsx b/packages/grid-shared-react/tests/options/Description.test.tsx new file mode 100644 index 0000000..145a800 --- /dev/null +++ b/packages/grid-shared-react/tests/options/Description.test.tsx @@ -0,0 +1,20 @@ +import { describe, it, expect } from 'vitest'; +import { Description } from '../../src/components/options/description/Description'; +import { getChildProps } from '../../src/utils/getChildProps'; + +describe('Description', () => { + it('maps description props and children into options.description', () => { + expect( + getChildProps( + + Monthly sales overview + + ) + ).toEqual({ + description: { + className: 'grid-description', + text: 'Monthly sales overview' + } + }); + }); +}); From 0c973162f03b7b9d52d044c1149d6285f6c98ad1 Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Fri, 10 Jul 2026 09:18:34 +0200 Subject: [PATCH 28/28] Added tests for Coolumns and ColumnDefaults. --- .../tests/options/Column.test.tsx | 54 +++++++++++++++++++ .../tests/options/ColumnDefaults.test.tsx | 27 ++++++++++ 2 files changed, 81 insertions(+) create mode 100644 packages/grid-shared-react/tests/options/Column.test.tsx create mode 100644 packages/grid-shared-react/tests/options/ColumnDefaults.test.tsx diff --git a/packages/grid-shared-react/tests/options/Column.test.tsx b/packages/grid-shared-react/tests/options/Column.test.tsx new file mode 100644 index 0000000..f52c073 --- /dev/null +++ b/packages/grid-shared-react/tests/options/Column.test.tsx @@ -0,0 +1,54 @@ +import { describe, it, expect } from 'vitest'; +import { Column } from '../../src/components/options/columns/Column'; +import { getChildProps } from '../../src/utils/getChildProps'; + +describe('Column', () => { + it('maps column props into options.columns', () => { + expect( + getChildProps( + + ) + ).toEqual({ + columns: [{ + width: 120, + sorting: { + enabled: true, + order: 'asc' + }, + header: { + format: '{value} USD' + }, + id: 'price' + }], + data: { + autogenerateColumns: false + } + }); + }); + + it('maps multiple columns into options.columns array', () => { + expect( + getChildProps( + <> + + + + ) + ).toEqual({ + columns: [ + { width: 200, id: 'product' }, + { width: 120, id: 'price' } + ], + data: { + autogenerateColumns: false + } + }); + }); +}); diff --git a/packages/grid-shared-react/tests/options/ColumnDefaults.test.tsx b/packages/grid-shared-react/tests/options/ColumnDefaults.test.tsx new file mode 100644 index 0000000..1a43555 --- /dev/null +++ b/packages/grid-shared-react/tests/options/ColumnDefaults.test.tsx @@ -0,0 +1,27 @@ +import { describe, it, expect } from 'vitest'; +import { ColumnDefaults } from '../../src/components/options/columns/ColumnDefaults'; +import { getChildProps } from '../../src/utils/getChildProps'; + +describe('ColumnDefaults', () => { + it('maps column defaults props into options.columnDefaults', () => { + expect( + getChildProps( + + ) + ).toEqual({ + columnDefaults: { + width: 160, + sorting: { + enabled: true + }, + cells: { + format: '{value}' + } + } + }); + }); +});