From 009f8ef100aa669b0dbce0f77dcb968e536b6c90 Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Mon, 31 Aug 2026 13:31:12 +0200 Subject: [PATCH 1/7] Fixed doclets and links in components. --- .../src/components/BaseGrid.tsx | 24 ++++-- .../components/options/caption/Caption.tsx | 14 +++- .../src/components/options/columns/Column.tsx | 5 ++ .../options/columns/ColumnDefaults.tsx | 11 ++- .../components/options/columns/columnProps.ts | 82 ++++++++++++++++++- .../src/components/options/data/Data.tsx | 31 ++++--- .../options/description/Description.tsx | 12 ++- .../src/components/options/header/Header.tsx | 5 ++ .../components/options/header/headerProps.ts | 29 ++++++- .../options/pagination/Pagination.tsx | 6 ++ .../options/pagination/paginationProps.ts | 39 ++++----- 11 files changed, 197 insertions(+), 61 deletions(-) diff --git a/packages/grid-shared-react/src/components/BaseGrid.tsx b/packages/grid-shared-react/src/components/BaseGrid.tsx index b9df08f..26410f7 100644 --- a/packages/grid-shared-react/src/components/BaseGrid.tsx +++ b/packages/grid-shared-react/src/components/BaseGrid.tsx @@ -15,21 +15,24 @@ import { } from '../hooks/useGrid'; /** - * Ref handle exposed by Grid components + * Ref handle exposed by Grid components. */ export interface GridRefHandle { /** - * Access to the underlying grid instance + * Access to the underlying grid instance. */ readonly grid: GridInstance | null; } /** - * Props for Grid component + * Props for the Grid component. */ export interface GridProps { /** - * Grid configuration options + * Grid options object. Merged with options from child + * components. + * + * Links to Grid.Options */ options?: TOptions; /** @@ -40,12 +43,15 @@ export interface GridProps { /** * Optional CSS class names mapped to Core `rendering.table.className` on * `.hcg-table`. Independent of `className` / `theme`. + * + * Links to Grid.Options.rendering.table.className */ tableClassName?: string; /** - * Optional theme name passed to Grid Core as `rendering.theme`. - * Omitted → Core default (`hcg-theme-default`). - * Defined (including `''`) → that value only. + * Omitted uses the Core default (`hcg-theme-default`). An empty string + * disables the theme. + * + * Links to Grid.Options.rendering.theme */ theme?: string; /** @@ -53,11 +59,11 @@ export interface GridProps { */ children?: ReactNode; /** - * Optional ref to access the grid instance + * Ref to access the grid instance. */ gridRef?: ForwardedRef>; /** - * Optional callback to be called when the grid is initialized + * Callback to be called when the grid is initialized. */ callback?: (grid: GridInstance) => void; } 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 99c9199..c811090 100644 --- a/packages/grid-shared-react/src/components/options/caption/Caption.tsx +++ b/packages/grid-shared-react/src/components/options/caption/Caption.tsx @@ -11,16 +11,26 @@ import { ReactNode } from 'react'; export interface CaptionProps { /** - * The custom CSS class name for the table caption. + * Links to Grid.Options.caption.className */ className?: string; /** - * The HTML tag to use for the caption. + * Links to Grid.Options.caption.htmlTag */ htmlTag?: string; + /** + * Caption text, passed as the component children. + * + * Links to Grid.Options.caption.text + */ children?: ReactNode; } +/** + * Table caption. Pass the caption text as children. + * + * Links to Grid.Options.caption + */ export function Caption(_props: CaptionProps) { return null; } 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 f5cd44a..944afd0 100644 --- a/packages/grid-shared-react/src/components/options/columns/Column.tsx +++ b/packages/grid-shared-react/src/components/options/columns/Column.tsx @@ -9,6 +9,11 @@ import type { ColumnProps } from './columnProps'; +/** + * Per-column configuration. Flattened React props map onto `columns[]`. + * + * Links to Grid.Options.columns + */ export function Column(_props: ColumnProps) { return null; } 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 d5a1faf..0a0d209 100644 --- a/packages/grid-shared-react/src/components/options/columns/ColumnDefaults.tsx +++ b/packages/grid-shared-react/src/components/options/columns/ColumnDefaults.tsx @@ -15,17 +15,20 @@ import type { ColumnOptionsProps } from './columnProps'; */ export interface ColumnDefaultsProps extends ColumnOptionsProps { /** - * CSS class names on every body ``. - * Maps to Core `rendering.rows.className`. + * Links to Grid.Options.rendering.rows.className */ rowClassName?: string; /** - * CSS class names on even body `` (Core `.hcg-row-even` parity). - * Maps to Core `rendering.rows.evenClassName`. + * Links to Grid.Options.rendering.rows.evenClassName */ evenRowClassName?: string; } +/** + * Default options applied to every column. + * + * Links to Grid.Options.columnDefaults + */ export function ColumnDefaults(_props: ColumnDefaultsProps) { return null; } 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 1452f99..ab14982 100644 --- a/packages/grid-shared-react/src/components/options/columns/columnProps.ts +++ b/packages/grid-shared-react/src/components/options/columns/columnProps.ts @@ -24,36 +24,107 @@ export interface CellValueGetterContext { * Shared column options (`columnDefaults` and per-column overrides). */ export interface ColumnOptionsProps { + /** + * Links to Grid.Options.columnDefaults.dataType + */ dataType?: ColumnDataType; + /** + * Links to Grid.Options.columnDefaults.width + */ width?: number | string; + /** + * Links to Grid.Options.columnDefaults.sorting.enabled + */ sortingEnabled?: boolean; + /** + * Links to Grid.Options.columns.sorting.order + */ sortingOrder?: ColumnSortingOrder; + /** + * Links to Grid.Options.columns.sorting.priority + */ sortingPriority?: number; + /** + * Links to Grid.Options.columnDefaults.sorting.orderSequence + */ sortingOrderSequence?: ColumnSortingOrder[]; + /** + * Links to Grid.Options.columnDefaults.sorting.compare + */ sortingCompare?: (a: unknown, b: unknown) => number; + /** + * Links to Grid.Options.columnDefaults.filtering.enabled + */ filteringEnabled?: boolean; + /** + * Links to Grid.Options.columnDefaults.filtering.inline + */ filteringInline?: boolean; + /** + * Links to Grid.Options.columnDefaults.filtering.condition + */ filteringCondition?: string; + /** + * Links to Grid.Options.columnDefaults.filtering.value + */ filteringValue?: string | number | boolean | null; + /** + * Links to Grid.Options.columnDefaults.header.className + */ headerClassName?: string; + /** + * Links to Grid.Options.columnDefaults.header.format + */ headerFormat?: string; + /** + * Links to Grid.Options.columnDefaults.header.formatter + */ headerFormatter?: (this: unknown) => string; + /** + * Links to Grid.Options.columnDefaults.header.style + */ headerStyle?: unknown; + /** + * Links to Grid.Options.columnDefaults.cells.rowHeader + */ cellRowHeader?: boolean; + /** + * Links to Grid.Options.columnDefaults.cells.className + */ cellClassName?: string; + /** + * Links to Grid.Options.columnDefaults.cells.format + */ cellFormat?: string; + /** + * Links to Grid.Options.columnDefaults.cells.formatter + */ cellFormatter?: (this: unknown) => string; /** * Custom cell value resolver. `this` is the Grid table cell (`row.index` * is the row index in the presentation data). + * + * Links to Grid.Options.columnDefaults.cells.valueGetter */ cellValueGetter?: (this: CellValueGetterContext) => unknown; + /** + * Links to Grid.Options.columnDefaults.cells.contextMenu + */ cellContextMenu?: { enabled?: boolean; items?: unknown[]; }; + /** + * Links to Grid.Options.columnDefaults.cells.style + */ cellStyle?: unknown; + /** + * Links to Grid.Options.columnDefaults.style + */ style?: unknown; + /** + * Links to Grid.Options.columnDefaults.exportable + */ exportable?: boolean; } @@ -63,12 +134,17 @@ export interface ColumnProps extends ColumnOptionsProps { */ id?: string; /** - * References the column to configure (data field id). Maps header, cells, - * sorting, filtering, etc. to Grid Core column options. + * Data field this column configures. Becomes `columns[].id` in Grid Core. * - * Becomes `options.columns[].id` in Grid Core (same identifier). + * Links to Grid.Options.columns.id */ columnId?: string; + /** + * Links to Grid.Options.columns.className + */ className?: string; + /** + * Links to Grid.Options.columns.enabled + */ 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 7ec764a..4518da7 100644 --- a/packages/grid-shared-react/src/components/options/data/Data.tsx +++ b/packages/grid-shared-react/src/components/options/data/Data.tsx @@ -13,46 +13,43 @@ export type DataColumns = Record>; export interface DataProps { /** - * The type of the data provider. - * - * @default 'local' + * Links to Grid.Options.data.providerType */ providerType?: 'local' | string; /** - * Whether columns should be generated automatically from data source - * column ids. - * - * Defaults to `true`. When declarative `` components are used, - * the React wrapper sets this to `false` unless you pass this prop - * explicitly. + * When declarative `` components are used, the React wrapper sets + * this to `false` unless the prop is passed explicitly. * - * @default true + * Links to Grid.Options.data.autogenerateColumns */ autogenerateColumns?: boolean; /** - * Columns data to initialize the Grid with. + * Links to Grid.Options.data.columns */ columns?: DataColumns; /** - * Data table as a source of data for the grid. + * Links to Grid.Options.data.dataTable */ dataTable?: unknown; /** - * Connector instance or options used to populate the data table. + * Links to Grid.Options.data.connector */ connector?: unknown; /** - * Automatically update the grid when the data table changes. - * - * @default false + * Links to Grid.Options.data.updateOnChange */ updateOnChange?: boolean; /** - * The column ID that contains the stable, unique row IDs. + * Links to Grid.Options.data.idColumn */ idColumn?: string; } +/** + * Data source for the grid. + * + * Links to Grid.Options.data + */ export function Data(_props: DataProps) { return null; } diff --git a/packages/grid-shared-react/src/components/options/description/Description.tsx b/packages/grid-shared-react/src/components/options/description/Description.tsx index c6ee575..e30d55f 100644 --- a/packages/grid-shared-react/src/components/options/description/Description.tsx +++ b/packages/grid-shared-react/src/components/options/description/Description.tsx @@ -11,12 +11,22 @@ import { ReactNode } from 'react'; export interface DescriptionProps { /** - * The custom CSS class name for the description. + * Links to Grid.Options.description.className */ className?: string; + /** + * Description text, passed as the component children. + * + * Links to Grid.Options.description.text + */ children?: ReactNode; } +/** + * Table description. Pass the description text as children. + * + * Links to Grid.Options.description + */ export function Description(_props: DescriptionProps) { return null; } diff --git a/packages/grid-shared-react/src/components/options/header/Header.tsx b/packages/grid-shared-react/src/components/options/header/Header.tsx index b952ef3..2334a17 100644 --- a/packages/grid-shared-react/src/components/options/header/Header.tsx +++ b/packages/grid-shared-react/src/components/options/header/Header.tsx @@ -9,6 +9,11 @@ import type { HeaderProps } from './headerProps'; +/** + * Header tree: column order, inclusion, and grouping. + * + * Links to Grid.Options.header + */ export function Header(_props: HeaderProps) { return null; } diff --git a/packages/grid-shared-react/src/components/options/header/headerProps.ts b/packages/grid-shared-react/src/components/options/header/headerProps.ts index 86f57bb..f0ece2f 100644 --- a/packages/grid-shared-react/src/components/options/header/headerProps.ts +++ b/packages/grid-shared-react/src/components/options/header/headerProps.ts @@ -11,26 +11,47 @@ * Accessibility options for a header cell in the header tree. */ export interface HeaderCellAccessibilityProps { + /** + * Links to Grid.Options.header.accessibility.description + */ description?: string; } /** * Header node in the `header` tree. A group (with `columns`) or a leaf - * (with `columnId`). Mirrors Grid Core `GroupedHeaderOptions`. + * (with `columnId`). */ export interface GroupedHeaderOptions { + /** + * Links to Grid.Options.header.accessibility + */ accessibility?: HeaderCellAccessibilityProps; + /** + * Links to Grid.Options.header.format + */ format?: string; + /** + * Links to Grid.Options.header.className + */ className?: string; + /** + * Links to Grid.Options.header.columnId + */ columnId?: string; + /** + * Nested header entries. A string is a column id. + * + * Links to Grid.Options.header.columns + */ columns?: Array; } export interface HeaderProps { /** - * Header tree: column order, inclusion, and grouping. - * Each entry is a column id (`string`) or a {@link GroupedHeaderOptions} - * object. Maps to Grid Core `options.header`. + * Header tree: column order, inclusion, and grouping. Each entry is a + * column id (`string`) or a {@link GroupedHeaderOptions} object. + * + * Links to Grid.Options.header */ header?: Array; } 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 8a169d7..84dc0fd 100644 --- a/packages/grid-shared-react/src/components/options/pagination/Pagination.tsx +++ b/packages/grid-shared-react/src/components/options/pagination/Pagination.tsx @@ -9,6 +9,12 @@ import type { PaginationProps } from './paginationProps'; +/** + * Page size and pagination controls. Position in the JSX tree sets + * `pagination.position` (`top` before other components, `bottom` after). + * + * Links to Grid.Options.pagination + */ export function Pagination(_props: PaginationProps) { return null; } diff --git a/packages/grid-shared-react/src/components/options/pagination/paginationProps.ts b/packages/grid-shared-react/src/components/options/pagination/paginationProps.ts index 3e31579..f239dbb 100644 --- a/packages/grid-shared-react/src/components/options/pagination/paginationProps.ts +++ b/packages/grid-shared-react/src/components/options/pagination/paginationProps.ts @@ -9,69 +9,66 @@ 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. + * Defaults to `true` when the `` component is used. Pass + * `false` to disable pagination while keeping other options. + * + * Links to Grid.Options.pagination.enabled */ enabled?: boolean; /** - * Additional CSS class name(s) for the pagination container - * (`.hcg-pagination`). + * Links to Grid.Options.pagination.className */ className?: string; /** - * Additional CSS class name(s) for the page info element - * (`.hcg-pagination-info`). + * Links to Grid.Options.pagination.controls.pageInfo.className */ infoClassName?: string; /** - * Additional CSS class name(s) for the controls container - * (`.hcg-pagination-controls`). + * Links to Grid.Options.pagination.controls.className */ controlsClassName?: string; /** - * Additional CSS class name(s) for the page size container - * (`.hcg-pagination-page-size`). + * Links to Grid.Options.pagination.controls.pageSizeSelector.className */ sizeClassName?: string; /** - * The current page number. + * Links to Grid.Options.pagination.page */ page?: number; /** - * Number of rows per page. + * Links to Grid.Options.pagination.pageSize */ pageSize?: number; /** - * Alignment of pagination elements within the wrapper. + * Links to Grid.Options.pagination.align */ align?: 'left' | 'center' | 'right' | 'distributed'; /** - * Whether to show the page information text. + * Links to Grid.Options.pagination.controls.pageInfo */ pageInfo?: boolean; /** - * Whether to show the page size selector. + * Links to Grid.Options.pagination.controls.pageSizeSelector */ pageSizeSelector?: boolean; /** - * Available options for the page size selector dropdown. + * Links to Grid.Options.pagination.controls.pageSizeSelector.options */ pageSizeOptions?: number[]; /** - * Whether to show numbered page buttons. + * Links to Grid.Options.pagination.controls.pageButtons */ pageButtons?: boolean; /** - * Maximum number of page number buttons to show before using ellipsis. + * Links to Grid.Options.pagination.controls.pageButtons.count */ pageButtonsCount?: number; /** - * Whether to show the first and last page navigation buttons. + * Links to Grid.Options.pagination.controls.firstLastButtons */ firstLast?: boolean; /** - * Whether to show the previous and next page navigation buttons. + * Links to Grid.Options.pagination.controls.previousNextButtons */ previousNext?: boolean; } From 05d702a5c9bd7bab9b29844c86c9fc81960468df Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Mon, 31 Aug 2026 14:19:33 +0200 Subject: [PATCH 2/7] Updated doclets in utils and Grid components. --- packages/grid-lite-react/src/Grid.tsx | 5 +++ packages/grid-pro-react/src/Grid.tsx | 5 +++ .../src/utils/mappers/column/columnOptions.ts | 45 +++++++++++++++++-- .../src/utils/mappers/grid/gridOptions.ts | 37 +++++++++++++-- .../mappers/pagination/paginationOptions.ts | 14 +++++- 5 files changed, 99 insertions(+), 7 deletions(-) diff --git a/packages/grid-lite-react/src/Grid.tsx b/packages/grid-lite-react/src/Grid.tsx index d6aec44..d14dec8 100644 --- a/packages/grid-lite-react/src/Grid.tsx +++ b/packages/grid-lite-react/src/Grid.tsx @@ -17,6 +17,11 @@ import type { Options } from '@highcharts/grid-lite/es-modules/Grid/Core/Options import type { GridProps } from '@highcharts/grid-shared-react'; import { buildGridOptions } from './utils/buildGridOptions'; +/** + * Grid Lite React component. + * + * Links to Grid.Options + */ export default function GridLite(props: GridProps) { const { gridRef, diff --git a/packages/grid-pro-react/src/Grid.tsx b/packages/grid-pro-react/src/Grid.tsx index 7400e61..4c5d134 100644 --- a/packages/grid-pro-react/src/Grid.tsx +++ b/packages/grid-pro-react/src/Grid.tsx @@ -19,6 +19,11 @@ import { } from './utils/mappers/grid'; import { buildGridOptions } from './utils/buildGridOptions'; +/** + * Grid Pro React component. + * + * Links to Grid.Options + */ export default function GridPro(props: GridProProps) { const { gridRef, children, options, callback, className } = props; const { gridOptions, columnKey } = useDeclarativeGridOptions( diff --git a/packages/grid-pro-react/src/utils/mappers/column/columnOptions.ts b/packages/grid-pro-react/src/utils/mappers/column/columnOptions.ts index a1e3bce..50c439a 100644 --- a/packages/grid-pro-react/src/utils/mappers/column/columnOptions.ts +++ b/packages/grid-pro-react/src/utils/mappers/column/columnOptions.ts @@ -16,33 +16,72 @@ import type { } from '@highcharts/grid-pro/es-modules/Grid/Pro/GridEvents.js'; /** - * Column-level event props mapped to `columns[].events`. + * Column-level event props mapped to `columns[].events`. Grid Pro. */ export interface ColumnLevelEventProps { + /** + * Links to Grid.Options.columns.events.afterResize + */ onAfterResize?: ColumnEventCallback; + /** + * Links to Grid.Options.columns.events.beforeSort + */ onBeforeSort?: ColumnEventCallback; + /** + * Links to Grid.Options.columns.events.afterSort + */ onAfterSort?: ColumnEventCallback; + /** + * Links to Grid.Options.columns.events.beforeFilter + */ onBeforeFilter?: ColumnEventCallback; + /** + * Links to Grid.Options.columns.events.afterFilter + */ onAfterFilter?: ColumnEventCallback; } /** - * Cell-level event props mapped to `columns[].cells.events`. + * Cell-level event props mapped to `columns[].cells.events`. Grid Pro. */ export interface CellLevelEventProps { + /** + * Links to Grid.Options.columns.cells.events.click + */ onCellClick?: CellEventCallback; + /** + * Links to Grid.Options.columns.cells.events.dblClick + */ onCellDblClick?: CellEventCallback; + /** + * Links to Grid.Options.columns.cells.events.mouseOver + */ onCellMouseOver?: CellEventCallback; + /** + * Links to Grid.Options.columns.cells.events.mouseOut + */ onCellMouseOut?: CellEventCallback; + /** + * Links to Grid.Options.columns.cells.events.afterRender + */ onCellAfterRender?: CellEventCallback; + /** + * Links to Grid.Options.columns.cells.events.afterEdit + */ onCellAfterEdit?: CellEventCallback; } /** - * Header-level event props mapped to `columns[].header.events`. + * Header-level event props mapped to `columns[].header.events`. Grid Pro. */ export interface HeaderLevelEventProps { + /** + * Links to Grid.Options.columns.header.events.click + */ onHeaderClick?: ColumnEventCallback; + /** + * Links to Grid.Options.columns.header.events.afterRender + */ onHeaderAfterRender?: ColumnEventCallback; } diff --git a/packages/grid-pro-react/src/utils/mappers/grid/gridOptions.ts b/packages/grid-pro-react/src/utils/mappers/grid/gridOptions.ts index 4d62502..e9989a6 100644 --- a/packages/grid-pro-react/src/utils/mappers/grid/gridOptions.ts +++ b/packages/grid-pro-react/src/utils/mappers/grid/gridOptions.ts @@ -29,25 +29,54 @@ export type GridProOptions = GridPro.Options & { export type GridOptions = GridProOptions; /** - * Grid-level event props mapped to `options.events`. + * Grid-level event props mapped to `options.events`. Grid Pro. */ export interface GridLevelEventProps { + /** + * Links to Grid.Options.events.beforeLoad + */ onBeforeLoad?: GridEventCallback; + /** + * Links to Grid.Options.events.afterLoad + */ onAfterLoad?: GridEventCallback; + /** + * Links to Grid.Options.events.beforeUpdate + */ onBeforeUpdate?: GridEventCallback; + /** + * Links to Grid.Options.events.afterUpdate + */ onAfterUpdate?: GridEventCallback; + /** + * Links to Grid.Options.events.beforeRedraw + */ onBeforeRedraw?: GridEventCallback; + /** + * Links to Grid.Options.events.afterRedraw + */ onAfterRedraw?: GridEventCallback; + /** + * Links to Grid.Options.events.beforeTreeRowToggle + */ onBeforeTreeRowToggle?: (e: BeforeTreeRowToggleEvent) => void; + /** + * Links to Grid.Options.events.afterTreeRowToggle + */ onAfterTreeRowToggle?: (e: AfterTreeRowToggleEvent) => void; } /** - * Row pinning event props mapped to - * `options.rendering.rows.pinning.events`. + * Row pinning event props. Grid Pro. */ export interface RowPinningEventProps { + /** + * Links to Grid.Options.rendering.rows.pinning.events.beforeRowPin + */ onBeforeRowPin?: RowPinningChangeEventCallback; + /** + * Links to Grid.Options.rendering.rows.pinning.events.afterRowPin + */ onAfterRowPin?: RowPinningChangeEventCallback; } @@ -60,6 +89,8 @@ export interface GridProProps extends BaseGridProps, GridEventProps { /** * Grid Pro license key. + * + * Links to Grid.Options.gridKey */ gridKey: string; } diff --git a/packages/grid-pro-react/src/utils/mappers/pagination/paginationOptions.ts b/packages/grid-pro-react/src/utils/mappers/pagination/paginationOptions.ts index 855a0c8..9902b48 100644 --- a/packages/grid-pro-react/src/utils/mappers/pagination/paginationOptions.ts +++ b/packages/grid-pro-react/src/utils/mappers/pagination/paginationOptions.ts @@ -18,12 +18,24 @@ import type { } from '@highcharts/grid-pro/es-modules/Grid/Pro/Pagination/PaginationComposition.js'; /** - * Pagination event props mapped to `pagination.events`. + * Pagination event props mapped to `pagination.events`. Grid Pro. */ export interface PaginationEventProps { + /** + * Links to Grid.Options.pagination.events.beforePageChange + */ onBeforePageChange?: (e: BeforePageChangeEvent) => void; + /** + * Links to Grid.Options.pagination.events.afterPageChange + */ onAfterPageChange?: (e: AfterPageChangeEvent) => void; + /** + * Links to Grid.Options.pagination.events.beforePageSizeChange + */ onBeforePageSizeChange?: (e: BeforePageSizeChangeEvent) => void; + /** + * Links to Grid.Options.pagination.events.afterPageSizeChange + */ onAfterPageSizeChange?: (e: AfterPageSizeChangeEvent) => void; } From a0d3d091386a7fc61b10d28e6b2c2078b98ff288 Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Tue, 1 Sep 2026 13:06:46 +0200 Subject: [PATCH 3/7] Added tree api generator. --- .gitignore | 2 + eslint.config.js | 2 +- package.json | 5 +- scripts/api-docs/api-grid-react.ts | 51 +++ scripts/api-docs/extractGridReact.test.ts | 124 ++++++ scripts/api-docs/extractGridReact.ts | 436 ++++++++++++++++++++++ vitest.api-docs.config.ts | 9 + 7 files changed, 626 insertions(+), 3 deletions(-) create mode 100644 scripts/api-docs/api-grid-react.ts create mode 100644 scripts/api-docs/extractGridReact.test.ts create mode 100644 scripts/api-docs/extractGridReact.ts create mode 100644 vitest.api-docs.config.ts diff --git a/.gitignore b/.gitignore index 9b03044..8459641 100644 --- a/.gitignore +++ b/.gitignore @@ -64,5 +64,7 @@ vite.config.ts.timestamp-* .turbo/ *.DS_Store +tmp/ + # Test artifacts __screenshots__/ diff --git a/eslint.config.js b/eslint.config.js index a934fae..8274e08 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -44,7 +44,7 @@ export default defineConfig( }, }, { - files: ['scripts/**/*.js', '**/next.config.js'], + files: ['scripts/**/*.{js,ts}', '**/next.config.js'], languageOptions: { globals: { ...globals.node, diff --git a/package.json b/package.json index 294f8e7..66d19eb 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,9 @@ "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", + "test:api-docs": "vitest run --config vitest.api-docs.config.ts", + "api-docs": "node scripts/api-docs/api-grid-react.ts", + "check": "pnpm lint && pnpm test && pnpm test:api-docs", "build": "pnpm -r --filter './packages/*' run build", "lint": "eslint packages examples scripts --ext .ts,.tsx,.js", "clean": "pnpm -r --filter './{packages,examples}/*' run clean && rimraf node_modules", diff --git a/scripts/api-docs/api-grid-react.ts b/scripts/api-docs/api-grid-react.ts new file mode 100644 index 0000000..0746de6 --- /dev/null +++ b/scripts/api-docs/api-grid-react.ts @@ -0,0 +1,51 @@ +/** + * Grid React integration. + * Copyright (c) 2025, Highsoft + * + * A valid license is required for using this software. + * See highcharts.com/license + * + * Usage: + * node scripts/api-docs/api-grid-react.ts [--out ] + * + */ + +import FS from 'node:fs'; +import Path from 'node:path'; +import Process from 'node:process'; +import { extractGridReactTree } from './extractGridReact.ts'; + +const repoRoot = Path.resolve(import.meta.dirname, '../..'); + +function readArg(flag: string): string | undefined { + const index = Process.argv.indexOf(flag); + if (index < 0) { + return undefined; + } + return Process.argv[index + 1]; +} + +const outPath = Path.resolve( + repoRoot, + readArg('--out') || 'tmp/tree-grid-react.json' +); + +const tree = extractGridReactTree({ + liteDts: Path.join( + repoRoot, + 'packages/grid-lite-react/dist/index.d.ts' + ), + proDts: Path.join( + repoRoot, + 'packages/grid-pro-react/dist/index.d.ts' + ), + litePackageRoot: Path.join(repoRoot, 'packages/grid-lite-react') +}); + +FS.mkdirSync(Path.dirname(outPath), { recursive: true }); +FS.writeFileSync(outPath, JSON.stringify(tree, null, 4) + '\n'); + +const categories = Object.keys(tree).filter((key) => key !== '_meta'); +Process.stdout.write( + `Wrote ${outPath} (${categories.length} categories).\n` +); diff --git a/scripts/api-docs/extractGridReact.test.ts b/scripts/api-docs/extractGridReact.test.ts new file mode 100644 index 0000000..79b38df --- /dev/null +++ b/scripts/api-docs/extractGridReact.test.ts @@ -0,0 +1,124 @@ +import { describe, expect, it } from 'vitest'; +import FS from 'node:fs'; +import Path from 'node:path'; +import { + extractFromDts, + extractGridReactTree, + mergeComponents +} from './extractGridReact.ts'; + +const repoRoot = Path.resolve(import.meta.dirname, '../..'); +const liteDts = Path.join( + repoRoot, + 'packages/grid-lite-react/dist/index.d.ts' +); +const proDts = Path.join( + repoRoot, + 'packages/grid-pro-react/dist/index.d.ts' +); + +function requireDts(): void { + if (!FS.existsSync(liteDts) || !FS.existsSync(proDts)) { + throw new Error('Missing dist/*.d.ts. Run `pnpm build` first.'); + } +} + +describe('extractGridReact', () => { + it('maps Caption.children to caption.text', () => { + requireDts(); + const caption = extractFromDts( + liteDts, + '@highcharts/grid-lite-react', + 'grid-lite-react/index.d.ts' + ).find((component) => component.name === 'Caption'); + + expect(caption).toBeDefined(); + const children = caption?.props.find((prop) => prop.name === 'children'); + expect(children?.hrefPath).toBe('caption.text'); + expect(children?.description).toMatch(/children/i); + }); + + it('maps Column.headerFormat to columnDefaults.header.format', () => { + requireDts(); + const column = extractFromDts( + liteDts, + '@highcharts/grid-lite-react', + 'grid-lite-react/index.d.ts' + ).find((component) => component.name === 'Column'); + + expect(column).toBeDefined(); + const headerFormat = column?.props.find( + (prop) => prop.name === 'headerFormat' + ); + expect(headerFormat?.hrefPath).toBe('columnDefaults.header.format'); + }); + + it('keeps React-only Grid.className without a crossref', () => { + requireDts(); + const grid = extractFromDts( + liteDts, + '@highcharts/grid-lite-react', + 'grid-lite-react/index.d.ts' + ).find((component) => component.name === 'Grid'); + + expect(grid).toBeDefined(); + const className = grid?.props.find((prop) => prop.name === 'className'); + expect(className?.hrefPath).toBeUndefined(); + }); + + it('merges Pro event props onto Column', () => { + requireDts(); + const lite = extractFromDts( + liteDts, + '@highcharts/grid-lite-react', + 'grid-lite-react/index.d.ts' + ); + const pro = extractFromDts( + proDts, + '@highcharts/grid-pro-react', + 'grid-pro-react/index.d.ts' + ); + const column = mergeComponents(lite, pro).find( + (component) => component.name === 'Column' + ); + const click = column?.props.find( + (prop) => prop.name === 'onCellClick' + ); + + expect(click?.hrefPath).toBe('columns.cells.events.click'); + }); + + it('writes tree-react contract for Caption.children', () => { + requireDts(); + const tree = extractGridReactTree({ + liteDts, + proDts, + litePackageRoot: Path.join( + repoRoot, + 'packages/grid-lite-react' + ) + }); + const caption = ( + tree.Components as { + children: Record; + }>; + } + ).children.Caption.children.children; + + expect(caption.doclet.crossref).toEqual([ + 'grid', + 'options/caption/text' + ]); + expect(tree._meta).toEqual(expect.objectContaining({ + version: expect.any(String), + branch: expect.any(String), + commit: expect.any(String) + })); + }); +}); diff --git a/scripts/api-docs/extractGridReact.ts b/scripts/api-docs/extractGridReact.ts new file mode 100644 index 0000000..8e01f4a --- /dev/null +++ b/scripts/api-docs/extractGridReact.ts @@ -0,0 +1,436 @@ +/** + * Grid React integration. + * Copyright (c) 2025, Highsoft + * + * A valid license is required for using this software. + * See highcharts.com/license + * + */ + +import { execSync } from 'node:child_process'; +import FS from 'node:fs'; +import Path from 'node:path'; +import ts from 'typescript'; + +const PRODUCT = 'grid'; +const COMPONENTS = new Set([ + 'Grid', 'Caption', 'Description', 'Data', 'Header', + 'Pagination', 'Column', 'ColumnDefaults' +]); +const SKIP = new Set(['GridLite', 'GridPro']); +const LINKS_RE = /Links to Grid\.Options(?:\.([\w.]+))?/; +const CATEGORY_COPY = { + Grid: 'Top-level Grid React component.', + Components: 'Declarative option components passed as Grid children.' +} as const; + +export interface PropEntry { + name: string; + type: string; + description: string; + hrefPath?: string; +} + +export interface ComponentDoc { + name: string; + category: keyof typeof CATEGORY_COPY; + importPath: string; + description: string; + props: PropEntry[]; + sourceFile: string; + hrefPath?: string; +} + +interface SourceIndex { + interfaces: Map; + aliases: Map; + exports: Map; + fns: Map; + vars: Map; +} + +function jsDoc(node: ts.Node, src: ts.SourceFile): string { + const text = src.getFullText(); + const blocks = (ts.getLeadingCommentRanges( + text, + node.getFullStart() + ) ?? []) + .map((range) => text.slice(range.pos, range.end)) + .filter((raw) => ( + raw.trimStart().startsWith('/**') && + !/A valid license is required/.test(raw) + )); + const raw = blocks.at(-1); + if (!raw) { + return ''; + } + return raw + .replace(/^\/\*\*?/, '') + .replace(/\*\/$/, '') + .split('\n') + .map((line) => line.replace(/^\s*\*\s?/, '')) + .join('\n') + .trim(); +} + +function parseDoc(raw: string): { + description: string; + hrefPath?: string; +} { + const match = raw.match(LINKS_RE); + const description = raw + .replace(LINKS_RE, '') + .replace(/\n{2,}/g, '\n') + .trim(); + return match ? + { description, hrefPath: match[1] ?? '' } : + { description }; +} + +function refName(type: ts.TypeNode): string | undefined { + if (ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName)) { + return type.typeName.text; + } + if ( + ts.isExpressionWithTypeArguments(type) && + ts.isIdentifier(type.expression) + ) { + return type.expression.text; + } + return undefined; +} + +function propsTypeName(typeText: string): string | undefined { + return typeText.match( + /(?:ComponentType|FC|FunctionComponent)\s*<\s*([A-Z]\w*)/ + )?.[1] ?? typeText.match(/^([A-Z]\w*)/)?.[1]; +} + +function collectProps( + typeName: string, + src: ts.SourceFile, + index: SourceIndex +): PropEntry[] { + const out = new Map(); + const seen = new Set(); + + const addProp = (member: ts.TypeElement): void => { + if (!ts.isPropertySignature(member) || !member.name) { + return; + } + const name = member.name.getText(src); + const parsed = parseDoc(jsDoc(member, src)); + out.set(name, { + name, + type: member.type ? + member.type.getText(src).replace(/\s+/g, ' ').trim() : + 'any', + description: parsed.description, + hrefPath: parsed.hrefPath + }); + }; + + const walkType = (type: ts.TypeNode): void => { + if (ts.isParenthesizedTypeNode(type)) { + walkType(type.type); + return; + } + if (ts.isIntersectionTypeNode(type)) { + type.types.forEach(walkType); + return; + } + if (ts.isTypeLiteralNode(type)) { + type.members.forEach(addProp); + return; + } + const name = refName(type); + if (name && !seen.has(name)) { + seen.add(name); + walkNamed(name); + } + }; + + const walkNamed = (name: string): void => { + const iface = index.interfaces.get(name); + if (iface) { + for (const clause of iface.heritageClauses ?? []) { + clause.types.forEach(walkType); + } + iface.members.forEach(addProp); + return; + } + const alias = index.aliases.get(name); + if (alias) { + walkType(alias.type); + } + }; + + walkNamed(typeName); + return [...out.values()]; +} + +function indexSource(src: ts.SourceFile): SourceIndex { + const index: SourceIndex = { + interfaces: new Map(), + aliases: new Map(), + exports: new Map(), + fns: new Map(), + vars: new Map() + }; + + for (const stmt of src.statements) { + if (ts.isInterfaceDeclaration(stmt)) { + index.interfaces.set(stmt.name.text, stmt); + } else if (ts.isTypeAliasDeclaration(stmt)) { + index.aliases.set(stmt.name.text, stmt); + } else if (ts.isFunctionDeclaration(stmt) && stmt.name) { + index.fns.set(stmt.name.text, stmt); + } else if (ts.isVariableStatement(stmt)) { + for (const decl of stmt.declarationList.declarations) { + if (ts.isIdentifier(decl.name)) { + index.vars.set(decl.name.text, decl); + } + } + } else if ( + ts.isExportDeclaration(stmt) && + stmt.exportClause && + ts.isNamedExports(stmt.exportClause) + ) { + for (const el of stmt.exportClause.elements) { + index.exports.set( + el.name.text, + el.propertyName?.text ?? el.name.text + ); + } + } + } + + return index; +} + +function declJsDoc( + fn: ts.FunctionDeclaration | undefined, + variable: ts.VariableDeclaration | undefined, + src: ts.SourceFile +): string { + if (fn) { + return jsDoc(fn, src); + } + const statement = variable?.parent?.parent; + return statement ? jsDoc(statement, src) : ''; +} + +export function extractFromDts( + dtsPath: string, + importPath: string, + sourceFile: string +): ComponentDoc[] { + const resolved = Path.resolve(dtsPath); + if (!FS.existsSync(resolved)) { + throw new Error(`Declaration file not found: ${resolved}`); + } + + const program = ts.createProgram([resolved], { + noEmit: true, + skipLibCheck: true, + jsx: ts.JsxEmit.ReactJSX + }); + const src = program.getSourceFile(resolved); + if (!src) { + throw new Error(`Could not parse ${resolved}`); + } + + const index = indexSource(src); + const docs: ComponentDoc[] = []; + + for (const [exportName, localName] of index.exports) { + if (SKIP.has(exportName) || !COMPONENTS.has(exportName)) { + continue; + } + const fn = index.fns.get(localName); + const variable = index.vars.get(localName); + if (!fn && !variable) { + continue; + } + + const typeText = fn?.parameters[0]?.type?.getText(src) ?? + variable?.type?.getText(src); + const typeName = typeText && propsTypeName(typeText); + const parsed = parseDoc(declJsDoc(fn, variable, src)); + + docs.push({ + name: exportName, + category: exportName === 'Grid' ? 'Grid' : 'Components', + importPath, + description: parsed.description, + props: typeName ? collectProps(typeName, src, index) : [], + sourceFile, + hrefPath: parsed.hrefPath + }); + } + + return docs; +} + +export function mergeComponents( + lite: ComponentDoc[], + pro: ComponentDoc[] +): ComponentDoc[] { + const byName = new Map(); + + for (const component of [...lite, ...pro]) { + const current = byName.get(component.name); + if (!current) { + byName.set(component.name, { + ...component, + props: [...component.props] + }); + continue; + } + const props = new Map(current.props.map((p) => [p.name, p])); + for (const prop of component.props) { + if (!props.has(prop.name)) { + props.set(prop.name, prop); + } + } + current.props = [...props.values()]; + current.description ||= component.description; + current.hrefPath ??= component.hrefPath; + } + + return [...byName.values()].sort((a, b) => ( + a.category.localeCompare(b.category) || + a.name.localeCompare(b.name) + )); +} + +function crossref(hrefPath?: string): string[] | undefined { + if (hrefPath === undefined) { + return undefined; + } + return [ + PRODUCT, + hrefPath ? `options/${hrefPath.replaceAll('.', '/')}` : 'options' + ]; +} + +function treeNode( + fullname: string, + name: string, + file: string | undefined, + doclet: Record, + hrefPath?: string, + children?: Record +): Record { + const xref = crossref(hrefPath); + return { + doclet: xref ? { ...doclet, crossref: xref } : doclet, + meta: { fullname, name, ...(file ? { file } : {}) }, + ...(children ? { children } : {}) + }; +} + +function escapeHtml(text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>'); +} + +function componentBody(component: ComponentDoc): string { + const snippet = + `
import { ${component.name} } from ` +
+        `'${component.importPath}';
`; + return component.description ? + `${snippet}

${escapeHtml(component.description)}

` : + snippet; +} + +export function buildTree( + components: ComponentDoc[], + meta: { branch: string; commit: string; version: string } +): Record { + const tree: Record = { _meta: meta }; + + for (const category of ['Grid', 'Components'] as const) { + const listed = components.filter((c) => c.category === category); + if (!listed.length) { + continue; + } + const children: Record = {}; + for (const c of listed) { + const props: Record = {}; + for (const p of c.props) { + props[p.name] = treeNode( + `${category}.${c.name}.${p.name}`, + p.name, + c.sourceFile, + { + description: p.description, + type: { names: [p.type] } + }, + p.hrefPath + ); + } + children[c.name] = treeNode( + `${category}.${c.name}`, + c.name, + c.sourceFile, + { description: componentBody(c) }, + c.hrefPath, + props + ); + } + tree[category] = treeNode( + category, + category, + undefined, + { description: CATEGORY_COPY[category] }, + undefined, + children + ); + } + + return tree; +} + +function git(cwd: string, args: string): string { + return execSync(`git ${args}`, { cwd, encoding: 'utf8' }).trim(); +} + +export function extractGridReactTree(options: { + liteDts: string; + proDts: string; + litePackageRoot: string; +}): Record { + const pkg = JSON.parse( + FS.readFileSync( + Path.join(options.litePackageRoot, 'package.json'), + 'utf8' + ) + ) as { version?: string }; + if (!pkg.version) { + throw new Error('Missing package version.'); + } + + const cwd = options.litePackageRoot; + return buildTree( + mergeComponents( + extractFromDts( + options.liteDts, + '@highcharts/grid-lite-react', + 'grid-lite-react/index.d.ts' + ), + extractFromDts( + options.proDts, + '@highcharts/grid-pro-react', + 'grid-pro-react/index.d.ts' + ) + ), + { + branch: git(cwd, 'rev-parse --abbrev-ref HEAD'), + commit: git(cwd, 'rev-parse --short HEAD'), + version: pkg.version + } + ); +} diff --git a/vitest.api-docs.config.ts b/vitest.api-docs.config.ts new file mode 100644 index 0000000..4a15e37 --- /dev/null +++ b/vitest.api-docs.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['scripts/api-docs/**/*.test.ts'], + environment: 'node', + globals: false + } +}); From 29f7060e0616f95f07df8a053f544430596ebc9a Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Tue, 1 Sep 2026 13:08:33 +0200 Subject: [PATCH 4/7] Linted. --- eslint.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.js b/eslint.config.js index 8274e08..180e973 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -38,7 +38,7 @@ export default defineConfig( ignoreStrings: true, ignoreTemplateLiterals: true }], - // Core hooks rules (skip React Compiler suite from flat.recommended). + // Core hooks rules. 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn', }, From ae4cdd9b3e92a5b89994625b62f29feb5ee154b5 Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Tue, 1 Sep 2026 13:31:44 +0200 Subject: [PATCH 5/7] Fixed workflow. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 66d19eb..a868e21 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "test:watch": "vitest", "pretest:e2e": "pnpm build", "test:e2e": "vitest run --config vitest.e2e.config.ts", + "test:all": "pnpm test && pnpm test:e2e && pnpm test:api-docs", "test:api-docs": "vitest run --config vitest.api-docs.config.ts", "api-docs": "node scripts/api-docs/api-grid-react.ts", "check": "pnpm lint && pnpm test && pnpm test:api-docs", From 7559a0fddb9978f75aee101c7a3ffcc72744905d Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Wed, 2 Sep 2026 14:02:06 +0200 Subject: [PATCH 6/7] Fixed defaults and grid versions. --- .../options/pagination/paginationProps.ts | 2 + scripts/api-docs/extractGridReact.test.ts | 102 ++++++++++++++++-- scripts/api-docs/extractGridReact.ts | 63 ++++++++--- 3 files changed, 145 insertions(+), 22 deletions(-) diff --git a/packages/grid-shared-react/src/components/options/pagination/paginationProps.ts b/packages/grid-shared-react/src/components/options/pagination/paginationProps.ts index f239dbb..831b33e 100644 --- a/packages/grid-shared-react/src/components/options/pagination/paginationProps.ts +++ b/packages/grid-shared-react/src/components/options/pagination/paginationProps.ts @@ -12,6 +12,8 @@ export interface PaginationProps { * Defaults to `true` when the `` component is used. Pass * `false` to disable pagination while keeping other options. * + * @default true + * * Links to Grid.Options.pagination.enabled */ enabled?: boolean; diff --git a/scripts/api-docs/extractGridReact.test.ts b/scripts/api-docs/extractGridReact.test.ts index 79b38df..dd3b2f1 100644 --- a/scripts/api-docs/extractGridReact.test.ts +++ b/scripts/api-docs/extractGridReact.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; import FS from 'node:fs'; +import OS from 'node:os'; import Path from 'node:path'; import { extractFromDts, @@ -24,7 +25,7 @@ function requireDts(): void { } describe('extractGridReact', () => { - it('maps Caption.children to caption.text', () => { + it('omits Caption.children from the public API tree', () => { requireDts(); const caption = extractFromDts( liteDts, @@ -33,9 +34,13 @@ describe('extractGridReact', () => { ).find((component) => component.name === 'Caption'); expect(caption).toBeDefined(); - const children = caption?.props.find((prop) => prop.name === 'children'); - expect(children?.hrefPath).toBe('caption.text'); - expect(children?.description).toMatch(/children/i); + expect( + caption?.props.some((prop) => prop.name === 'children') + ).toBe(false); + const className = caption?.props.find( + (prop) => prop.name === 'className' + ); + expect(className?.hrefPath).toBe('caption.className'); }); it('maps Column.headerFormat to columnDefaults.header.format', () => { @@ -86,9 +91,13 @@ describe('extractGridReact', () => { ); expect(click?.hrefPath).toBe('columns.cells.events.click'); + expect(click?.proOnly).toBe(true); + expect( + column?.props.find((prop) => prop.name === 'headerFormat')?.proOnly + ).toBeFalsy(); }); - it('writes tree-react contract for Caption.children', () => { + it('writes tree-react contract for Caption.className', () => { requireDts(); const tree = extractGridReactTree({ liteDts, @@ -105,20 +114,97 @@ describe('extractGridReact', () => { doclet: { crossref?: string[]; description?: string; + product?: string; }; }>; }>; } - ).children.Caption.children.children; + ).children.Caption.children; - expect(caption.doclet.crossref).toEqual([ + expect(caption.children).toBeUndefined(); + expect(caption.className.doclet.crossref).toEqual([ 'grid', - 'options/caption/text' + 'options/caption/className' ]); + expect(caption.className.doclet.product).toBeUndefined(); + expect(caption.className.doclet.default).toBeUndefined(); expect(tree._meta).toEqual(expect.objectContaining({ version: expect.any(String), branch: expect.any(String), commit: expect.any(String) })); + + const grid = ( + tree.Grid as { + children: Record; + } + ).children.Grid; + expect(grid.doclet.description).toContain( + "from '@highcharts/grid-lite-react'; // or '@highcharts/grid-pro-react'" + ); + expect(grid.doclet.description) + .not.toMatch(/Grid Lite React component/); + + const captionPage = ( + tree.Components as { + children: Record; + } + ).children.Caption; + expect(captionPage.doclet.description).toContain( + "from '@highcharts/grid-lite-react'; // or '@highcharts/grid-pro-react'" + ); + + const column = ( + tree.Components as { + children: Record; + }>; + } + ).children.Column.children; + expect(column.onCellClick.doclet.product).toBe('gridpro'); + expect(column.headerFormat.doclet.product).toBeUndefined(); + }); + + it('extracts @default into the tree doclet and strips it from the description', () => { + const dir = FS.mkdtempSync(Path.join(OS.tmpdir(), 'grid-react-api-docs-')); + const dtsPath = Path.join(dir, 'index.d.ts'); + + FS.writeFileSync(dtsPath, ` +export interface PaginationProps { + /** + * Defaults to true when Pagination is used. + * + * @default true + * + * Links to Grid.Options.pagination.enabled + */ + enabled?: boolean; +} +declare function Pagination(props: PaginationProps): null; +export { Pagination }; +`); + + try { + const pagination = extractFromDts( + dtsPath, + '@highcharts/grid-lite-react', + 'grid-lite-react/index.d.ts' + ).find((component) => component.name === 'Pagination'); + const enabled = pagination?.props.find( + (prop) => prop.name === 'enabled' + ); + + expect(enabled?.defaultValue).toBe('true'); + expect(enabled?.hrefPath).toBe('pagination.enabled'); + expect(enabled?.description).toContain( + 'Defaults to true when Pagination is used.' + ); + expect(enabled?.description).not.toMatch(/@default/); + expect(enabled?.description).not.toMatch(/Links to Grid/); + } finally { + FS.rmSync(dir, { recursive: true, force: true }); + } }); }); diff --git a/scripts/api-docs/extractGridReact.ts b/scripts/api-docs/extractGridReact.ts index 8e01f4a..726566b 100644 --- a/scripts/api-docs/extractGridReact.ts +++ b/scripts/api-docs/extractGridReact.ts @@ -13,6 +13,8 @@ import Path from 'node:path'; import ts from 'typescript'; const PRODUCT = 'grid'; +const LITE_PKG = '@highcharts/grid-lite-react'; +const PRO_PKG = '@highcharts/grid-pro-react'; const COMPONENTS = new Set([ 'Grid', 'Caption', 'Description', 'Data', 'Header', 'Pagination', 'Column', 'ColumnDefaults' @@ -29,6 +31,8 @@ export interface PropEntry { type: string; description: string; hrefPath?: string; + defaultValue?: string; + proOnly?: boolean; } export interface ComponentDoc { @@ -73,18 +77,25 @@ function jsDoc(node: ts.Node, src: ts.SourceFile): string { .trim(); } +const DEFAULT_RE = /@default\s+(\S.*)$/m; + function parseDoc(raw: string): { description: string; hrefPath?: string; + defaultValue?: string; } { const match = raw.match(LINKS_RE); + const defaultMatch = raw.match(DEFAULT_RE); const description = raw .replace(LINKS_RE, '') + .replace(DEFAULT_RE, '') .replace(/\n{2,}/g, '\n') .trim(); - return match ? - { description, hrefPath: match[1] ?? '' } : - { description }; + return { + description, + ...(match ? { hrefPath: match[1] ?? '' } : {}), + ...(defaultMatch ? { defaultValue: defaultMatch[1].trim() } : {}) + }; } function refName(type: ts.TypeNode): string | undefined { @@ -119,6 +130,9 @@ function collectProps( return; } const name = member.name.getText(src); + if (name === 'children') { + return; + } const parsed = parseDoc(jsDoc(member, src)); out.set(name, { name, @@ -126,7 +140,8 @@ function collectProps( member.type.getText(src).replace(/\s+/g, ' ').trim() : 'any', description: parsed.description, - hrefPath: parsed.hrefPath + hrefPath: parsed.hrefPath, + defaultValue: parsed.defaultValue }); }; @@ -278,19 +293,29 @@ export function mergeComponents( ): ComponentDoc[] { const byName = new Map(); - for (const component of [...lite, ...pro]) { + for (const component of lite) { + byName.set(component.name, { + ...component, + props: component.props.map((prop) => ({ ...prop })) + }); + } + + for (const component of pro) { const current = byName.get(component.name); if (!current) { byName.set(component.name, { ...component, - props: [...component.props] + props: component.props.map((prop) => ({ + ...prop, + proOnly: true + })) }); continue; } const props = new Map(current.props.map((p) => [p.name, p])); for (const prop of component.props) { if (!props.has(prop.name)) { - props.set(prop.name, prop); + props.set(prop.name, { ...prop, proOnly: true }); } } current.props = [...props.values()]; @@ -337,12 +362,20 @@ function escapeHtml(text: string): string { .replace(/>/g, '>'); } +function importLine(name: string, pkg: string): string { + return `import { ${name} } from '${pkg}';`; +} + function componentBody(component: ComponentDoc): string { const snippet = - `
import { ${component.name} } from ` +
-        `'${component.importPath}';
`; - return component.description ? - `${snippet}

${escapeHtml(component.description)}

` : + `
${escapeHtml(
+            `${importLine(component.name, LITE_PKG)} // or '${PRO_PKG}'`
+        )}
`; + const description = component.name === 'Grid' ? + CATEGORY_COPY.Grid : + component.description; + return description ? + `${snippet}

${escapeHtml(description)}

` : snippet; } @@ -367,7 +400,9 @@ export function buildTree( c.sourceFile, { description: p.description, - type: { names: [p.type] } + type: { names: [p.type] }, + ...(p.defaultValue ? { default: p.defaultValue } : {}), + ...(p.proOnly ? { product: 'gridpro' } : {}) }, p.hrefPath ); @@ -418,12 +453,12 @@ export function extractGridReactTree(options: { mergeComponents( extractFromDts( options.liteDts, - '@highcharts/grid-lite-react', + LITE_PKG, 'grid-lite-react/index.d.ts' ), extractFromDts( options.proDts, - '@highcharts/grid-pro-react', + PRO_PKG, 'grid-pro-react/index.d.ts' ) ), From 3d57e89184feabff9b8e62ad979a1cf11a1f66fe Mon Sep 17 00:00:00 2001 From: Sebastian Bochan Date: Wed, 9 Sep 2026 10:41:26 +0200 Subject: [PATCH 7/7] Fix Grid React API extractor for const component declarations. Parse `(props: T) => null` from dist .d.ts so option components keep their props after the new options/id API, and lock the contract with tests. Co-authored-by: Cursor --- scripts/api-docs/extractGridReact.test.ts | 72 +++++++++++++++++++++++ scripts/api-docs/extractGridReact.ts | 51 ++++++++++++---- 2 files changed, 113 insertions(+), 10 deletions(-) diff --git a/scripts/api-docs/extractGridReact.test.ts b/scripts/api-docs/extractGridReact.test.ts index dd3b2f1..69c04f1 100644 --- a/scripts/api-docs/extractGridReact.test.ts +++ b/scripts/api-docs/extractGridReact.test.ts @@ -207,4 +207,76 @@ export { Pagination }; FS.rmSync(dir, { recursive: true, force: true }); } }); + + it('reads props from declare const X: (props: T) => null', () => { + const dir = FS.mkdtempSync(Path.join(OS.tmpdir(), 'grid-react-api-docs-')); + const dtsPath = Path.join(dir, 'index.d.ts'); + + FS.writeFileSync(dtsPath, ` +export interface CaptionProps { + /** + * Links to Grid.Options.caption.className + */ + className?: string; + /** + * Links to Grid.Options.caption + */ + options?: unknown; + children?: string; +} +declare const Caption: (props: CaptionProps) => null; +export { Caption }; +`); + + try { + const caption = extractFromDts( + dtsPath, + '@highcharts/grid-lite-react', + 'grid-lite-react/index.d.ts' + ).find((component) => component.name === 'Caption'); + const names = caption?.props.map((prop) => prop.name); + + expect(names).toEqual(['className', 'options']); + expect( + caption?.props.find((prop) => prop.name === 'className')?.hrefPath + ).toBe('caption.className'); + expect( + caption?.props.find((prop) => prop.name === 'options')?.hrefPath + ).toBe('caption'); + } finally { + FS.rmSync(dir, { recursive: true, force: true }); + } + }); + + it('maps Column.id, Header.options, and the options bag from dist', () => { + requireDts(); + const lite = extractFromDts( + liteDts, + '@highcharts/grid-lite-react', + 'grid-lite-react/index.d.ts' + ); + const column = lite.find((component) => component.name === 'Column'); + const header = lite.find((component) => component.name === 'Header'); + const caption = lite.find((component) => component.name === 'Caption'); + const data = lite.find((component) => component.name === 'Data'); + + expect(column?.props.find((prop) => prop.name === 'id')?.hrefPath) + .toBe('columns.id'); + expect(column?.props.some((prop) => prop.name === 'columnId')) + .toBe(false); + expect(column?.props.find((prop) => prop.name === 'dataId')?.hrefPath) + .toBe('columns.dataId'); + expect(column?.props.find((prop) => prop.name === 'options')?.hrefPath) + .toBe('columns'); + + expect(header?.props.find((prop) => prop.name === 'options')?.hrefPath) + .toBe('header'); + expect(header?.props.some((prop) => prop.name === 'header')) + .toBe(false); + + expect(caption?.props.find((prop) => prop.name === 'options')?.hrefPath) + .toBe('caption'); + expect(data?.props.find((prop) => prop.name === 'options')?.hrefPath) + .toBe('data'); + }); }); diff --git a/scripts/api-docs/extractGridReact.ts b/scripts/api-docs/extractGridReact.ts index 726566b..a52370d 100644 --- a/scripts/api-docs/extractGridReact.ts +++ b/scripts/api-docs/extractGridReact.ts @@ -114,7 +114,31 @@ function refName(type: ts.TypeNode): string | undefined { function propsTypeName(typeText: string): string | undefined { return typeText.match( /(?:ComponentType|FC|FunctionComponent)\s*<\s*([A-Z]\w*)/ - )?.[1] ?? typeText.match(/^([A-Z]\w*)/)?.[1]; + )?.[1] ?? + typeText.match(/\(\s*\w+\s*:\s*([A-Z]\w*)/)?.[1] ?? + typeText.match(/^([A-Z]\w*)/)?.[1]; +} + +function firstParamTypeName( + type: ts.TypeNode | undefined, + src: ts.SourceFile +): string | undefined { + if (!type) { + return undefined; + } + if (ts.isFunctionTypeNode(type)) { + return firstParamTypeName(type.parameters[0]?.type, src); + } + return refName(type) ?? propsTypeName(type.getText(src)); +} + +function isExported(node: ts.Node): boolean { + return Boolean( + ts.canHaveModifiers(node) && + ts.getModifiers(node)?.some( + (modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword + ) + ); } function collectProps( @@ -122,8 +146,8 @@ function collectProps( src: ts.SourceFile, index: SourceIndex ): PropEntry[] { - const out = new Map(); - const seen = new Set(); + const propsByName = new Map(); + const visitedTypeNames = new Set(); const addProp = (member: ts.TypeElement): void => { if (!ts.isPropertySignature(member) || !member.name) { @@ -134,7 +158,7 @@ function collectProps( return; } const parsed = parseDoc(jsDoc(member, src)); - out.set(name, { + propsByName.set(name, { name, type: member.type ? member.type.getText(src).replace(/\s+/g, ' ').trim() : @@ -159,8 +183,8 @@ function collectProps( return; } const name = refName(type); - if (name && !seen.has(name)) { - seen.add(name); + if (name && !visitedTypeNames.has(name)) { + visitedTypeNames.add(name); walkNamed(name); } }; @@ -181,7 +205,7 @@ function collectProps( }; walkNamed(typeName); - return [...out.values()]; + return [...propsByName.values()]; } function indexSource(src: ts.SourceFile): SourceIndex { @@ -200,10 +224,16 @@ function indexSource(src: ts.SourceFile): SourceIndex { index.aliases.set(stmt.name.text, stmt); } else if (ts.isFunctionDeclaration(stmt) && stmt.name) { index.fns.set(stmt.name.text, stmt); + if (isExported(stmt)) { + index.exports.set(stmt.name.text, stmt.name.text); + } } else if (ts.isVariableStatement(stmt)) { for (const decl of stmt.declarationList.declarations) { if (ts.isIdentifier(decl.name)) { index.vars.set(decl.name.text, decl); + if (isExported(stmt)) { + index.exports.set(decl.name.text, decl.name.text); + } } } } else if ( @@ -268,9 +298,10 @@ export function extractFromDts( continue; } - const typeText = fn?.parameters[0]?.type?.getText(src) ?? - variable?.type?.getText(src); - const typeName = typeText && propsTypeName(typeText); + const typeName = firstParamTypeName( + fn?.parameters[0]?.type ?? variable?.type, + src + ); const parsed = parseDoc(declJsDoc(fn, variable, src)); docs.push({