@genesislcap/foundation-react-utils 14.481.2-alpha-829ad82.0 → 14.482.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import type { ComponentFactory } from '@genesislcap/foundation-layout';
2
- import type { ComponentType } from 'react';
2
+ import { ComponentType } from 'react';
3
3
  import { JsonFormsState } from '@jsonforms/core';
4
4
  import { OwnPropsOfControl } from '@jsonforms/core';
5
5
  import { RankedTester } from '@jsonforms/core';
@@ -8,6 +8,110 @@ import { RendererControlProps } from '@genesislcap/foundation-forms';
8
8
  import { RendererEntry } from '@genesislcap/foundation-forms';
9
9
  import { StatePropsOfControl } from '@jsonforms/core';
10
10
 
11
+ /**
12
+ * Creates a {@link GridProCellPortals} manager: a single React root shared by every cell
13
+ * renderer registered with it, with each cell rendered into its grid cell via a portal.
14
+ *
15
+ * Use this instead of the default one-root-per-cell mode when cell renderers need to
16
+ * inherit React Context from the application tree (Redux, theme, router, query clients...)
17
+ * — portal-rendered cells live in the tree where `Portals` is rendered, so context updates
18
+ * propagate into visible cells like any other React state.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * const cellPortals = createGridProCellPortals();
23
+ *
24
+ * const gridOptions = {
25
+ * components: {
26
+ * priceCell: createGridProCellRenderer(PriceCell, { portals: cellPortals }),
27
+ * },
28
+ * columnDefs: [{ field: 'price', cellRenderer: 'priceCell' }],
29
+ * };
30
+ *
31
+ * function Page() {
32
+ * return (
33
+ * <SomeProvider>
34
+ * <cellPortals.Portals />
35
+ * <RapidGridPro gridOptions={gridOptions} />
36
+ * </SomeProvider>
37
+ * );
38
+ * }
39
+ * ```
40
+ *
41
+ * @remarks
42
+ * Trade-offs versus the default per-cell roots: all cells registered with one manager share
43
+ * a React tree, so a renderer that throws is contained by a per-cell error boundary (the
44
+ * failing cell renders empty rather than crashing the rest), and unmounting `Portals` while
45
+ * the grid is alive blanks every portal-rendered cell. Cell mounts are committed
46
+ * synchronously (so AG Grid measures real content), which re-renders the manager once per
47
+ * created cell — the per-cell portals are memoised, keeping that cheap at grid-pro's
48
+ * virtualised cell counts.
49
+ *
50
+ * @public
51
+ */
52
+ export declare function createGridProCellPortals(): GridProCellPortals;
53
+
54
+ /**
55
+ * Wraps a React component as an AG Grid cell renderer component class for use with
56
+ * `grid-pro` / `grid-pro-beta` (`<foundation-grid-pro>` / `<rapid-grid-pro>`).
57
+ *
58
+ * Grid Pro is a framework-agnostic Web Component, so AG Grid inside it only understands
59
+ * plain-JS component classes. This helper bridges the gap: it returns a class implementing
60
+ * `ICellRendererComp` that mounts the React component into the cell with its own React root,
61
+ * re-renders it on `refresh` (so value changes and cell flashing keep working without
62
+ * remounting), and unmounts it when the cell is destroyed.
63
+ *
64
+ * The component receives the full AG Grid `ICellRendererParams` object as props — including
65
+ * `value`, `data`, `node`, `api` and anything supplied via the column's `cellRendererParams`.
66
+ * Hooks and local state inside the component work as normal.
67
+ *
68
+ * Note on React Context/Redux: by default each cell is rendered into its own independent
69
+ * React root, so renderers do NOT inherit contexts (Redux providers, theme context, React
70
+ * Router, etc.) from the application tree hosting the grid. Either pass data/callbacks
71
+ * explicitly via `cellRendererParams`, or opt into portal mode with the `portals` option
72
+ * (see {@link GridProCellRendererOptions.portals} and `createGridProCellPortals`) — portal
73
+ * cells share the application tree and inherit its contexts.
74
+ *
75
+ * Note on `key`: props are passed by spreading the AG Grid params object, and React reserves
76
+ * `key` — a `cellRendererParams` entry named `key` is consumed as the element key and never
77
+ * reaches the component, so avoid that name. (`ref` is fine: React 19 forwards it to function
78
+ * components as a regular prop.)
79
+ *
80
+ * Note on styling: cells render inside grid-pro's shadow root, so document-level stylesheets
81
+ * (CSS files imported by your app/page) do NOT reach the rendered component. Style renderers
82
+ * with inline styles or CSS-in-JS. Design-system tokens remain usable either way — CSS custom
83
+ * properties inherit across shadow boundaries (e.g. `color: 'var(--accent-fill-rest)'`).
84
+ *
85
+ * @example
86
+ * ```tsx
87
+ * import { createGridProCellRenderer, type GridProCellRendererProps } from '@genesislcap/foundation-react-utils';
88
+ *
89
+ * function PriceCell({ value }: GridProCellRendererProps) {
90
+ * return <span style={{ color: value >= 0 ? 'green' : 'red' }}>{value}</span>;
91
+ * }
92
+ *
93
+ * const gridOptions = {
94
+ * components: {
95
+ * priceCell: createGridProCellRenderer(PriceCell),
96
+ * },
97
+ * columnDefs: [
98
+ * { field: 'price', cellRenderer: 'priceCell' },
99
+ * ],
100
+ * };
101
+ *
102
+ * // <rapid-grid-pro gridOptions={gridOptions} /> — or register via the grid's
103
+ * // `gridComponents` property to make the renderer available across all columns.
104
+ * ```
105
+ *
106
+ * @param Component - The React component to render inside the cell.
107
+ * @param options - Optional {@link GridProCellRendererOptions} to tune the wrapper behaviour.
108
+ * @returns A cell renderer component class registrable via `gridOptions.components`,
109
+ * the grid's `gridComponents` property, or directly as a column's `cellRenderer`.
110
+ *
111
+ * @public
112
+ */
113
+ export declare function createGridProCellRenderer<P extends GridProCellRendererProps = GridProCellRendererProps>(Component: ComponentType<P>, options?: GridProCellRendererOptions): new () => GridProReactCellRenderer;
114
+
11
115
  /**
12
116
  * Converts a React component into a `RendererEntry` for use with `foundation-form`'s
13
117
  * `additionalRenderers` property.
@@ -86,6 +190,124 @@ export declare function createReactRenderer(Component: ComponentType<RendererCon
86
190
  wrapWithControlWrapper?: boolean;
87
191
  }): RendererEntry;
88
192
 
193
+ /**
194
+ * Shared portal manager for Grid Pro React cell renderers.
195
+ *
196
+ * Created with {@link createGridProCellPortals}. Render the {@link GridProCellPortals.Portals}
197
+ * component exactly once inside your React tree — cells rendered through this manager become
198
+ * part of that tree, so they inherit React Context (Redux providers, theme, router, etc.) from
199
+ * wherever `Portals` sits. Pass the manager to {@link createGridProCellRenderer} via the
200
+ * `portals` option to opt a renderer into portal mode.
201
+ *
202
+ * @public
203
+ */
204
+ export declare interface GridProCellPortals {
205
+ /**
206
+ * Host component for all cells rendered through this manager. Render it exactly once,
207
+ * inside every provider the cell renderers should inherit, and keep it mounted for as
208
+ * long as the grid is on screen — unmounting it blanks all portal-rendered cells.
209
+ */
210
+ Portals: ComponentType;
211
+ /**
212
+ * Mounts a component into a cell container and returns the entry id.
213
+ * Called by the cell renderer wrapper — not intended for direct use.
214
+ * @internal
215
+ */
216
+ mount(container: HTMLElement, Component: ComponentType<any>, params: any): number;
217
+ /**
218
+ * Re-renders a mounted cell with new params.
219
+ * Called by the cell renderer wrapper — not intended for direct use.
220
+ * @internal
221
+ */
222
+ update(id: number, params: any): void;
223
+ /**
224
+ * Removes a mounted cell.
225
+ * Called by the cell renderer wrapper — not intended for direct use.
226
+ * @internal
227
+ */
228
+ remove(id: number): void;
229
+ }
230
+
231
+ /**
232
+ * Options for {@link createGridProCellRenderer}.
233
+ *
234
+ * @public
235
+ */
236
+ export declare interface GridProCellRendererOptions {
237
+ /**
238
+ * When true (the default), the cell container is styled as a full-size flex row
239
+ * (`display: flex; align-items: center; height: 100%; width: 100%`) so content is
240
+ * vertically centred like grid-pro's built-in renderers.
241
+ *
242
+ * Set to false to leave the container unstyled and let the cell's own layout apply.
243
+ * Do this when the forced flex/height layout gets in the way, e.g.:
244
+ * - `autoHeight`/`wrapText` columns — `height: 100%` is circular when the row height
245
+ * derives from the content, so the cell can collapse instead of growing;
246
+ * - right-aligned numeric cells relying on `text-align` (a flex item packs to the start);
247
+ * - content using `text-overflow: ellipsis` (a flex item won't truncate without
248
+ * `min-width: 0`).
249
+ */
250
+ fillCell?: boolean;
251
+ /**
252
+ * Render cells through a shared {@link GridProCellPortals} manager instead of giving each
253
+ * cell its own React root. Portal-rendered cells become part of the React tree where the
254
+ * manager's `Portals` component is rendered, so they inherit React Context (Redux, theme,
255
+ * router, etc.) from the application — the main limitation of the default mode.
256
+ *
257
+ * Create one with `createGridProCellPortals()` and render `<manager.Portals />` once,
258
+ * inside the providers the cells should see. When omitted (the default), each cell gets
259
+ * its own isolated root.
260
+ */
261
+ portals?: GridProCellPortals;
262
+ }
263
+
264
+ /**
265
+ * Props passed to a React component used as a Grid Pro cell renderer.
266
+ *
267
+ * This is a structural subset of AG Grid's `ICellRendererParams` — the full params object
268
+ * (including any custom `cellRendererParams` from the column definition) is spread onto the
269
+ * component as props on every render, so anything available on `ICellRendererParams` is
270
+ * available here. Declared locally to avoid coupling this package to a specific
271
+ * `ag-grid-community` version.
272
+ *
273
+ * @public
274
+ */
275
+ export declare interface GridProCellRendererProps<TData = any, TValue = any> {
276
+ /** The cell value. */
277
+ value: TValue;
278
+ /** Value formatted by the column's value formatter, if any. */
279
+ valueFormatted?: string | null;
280
+ /** The full row data. */
281
+ data: TData;
282
+ /** The AG Grid row node. */
283
+ node: any;
284
+ /** The AG Grid api. */
285
+ api: any;
286
+ /** The row index of the cell. */
287
+ rowIndex: number;
288
+ /** The grid context object, as supplied via `gridOptions.context`. */
289
+ context?: any;
290
+ /** The column this cell belongs to. */
291
+ column?: any;
292
+ /** The column definition. */
293
+ colDef?: any;
294
+ /** Custom params from `cellRendererParams` and any other AG Grid params. */
295
+ [key: string]: any;
296
+ }
297
+
298
+ /**
299
+ * The subset of AG Grid's `ICellRendererComp` contract implemented by the wrapper class
300
+ * returned from {@link createGridProCellRenderer}.
301
+ *
302
+ * @public
303
+ */
304
+ export declare interface GridProReactCellRenderer {
305
+ init(params: GridProCellRendererProps): void;
306
+ getGui(): HTMLElement;
307
+ refresh(params: GridProCellRendererProps): boolean;
308
+ destroy(): void;
309
+ }
310
+
89
311
  /**
90
312
  * Creates a factory function for rendering React components in layout items.
91
313
  *
@@ -1 +1 @@
1
- {"root":["../src/create-react-renderer.ts","../src/index.ts","../src/react-layout-factory.tsx"],"version":"5.9.2"}
1
+ {"root":["../src/create-grid-pro-cell-portals.tsx","../src/create-grid-pro-cell-renderer.ts","../src/create-react-renderer.ts","../src/index.ts","../src/react-layout-factory.tsx"],"version":"5.9.2"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@genesislcap/foundation-react-utils",
3
3
  "description": "Genesis Foundation React Utils",
4
- "version": "14.481.2-alpha-829ad82.0",
4
+ "version": "14.482.0",
5
5
  "sideEffects": false,
6
6
  "license": "SEE LICENSE IN license.txt",
7
7
  "main": "dist/esm/index.js",
@@ -29,23 +29,23 @@
29
29
  }
30
30
  },
31
31
  "devDependencies": {
32
- "@genesislcap/foundation-testing": "14.481.2-alpha-829ad82.0",
33
- "@genesislcap/genx": "14.481.2-alpha-829ad82.0",
34
- "@genesislcap/rollup-builder": "14.481.2-alpha-829ad82.0",
35
- "@genesislcap/ts-builder": "14.481.2-alpha-829ad82.0",
36
- "@genesislcap/uvu-playwright-builder": "14.481.2-alpha-829ad82.0",
37
- "@genesislcap/vite-builder": "14.481.2-alpha-829ad82.0",
38
- "@genesislcap/webpack-builder": "14.481.2-alpha-829ad82.0"
32
+ "@genesislcap/foundation-testing": "14.482.0",
33
+ "@genesislcap/genx": "14.482.0",
34
+ "@genesislcap/rollup-builder": "14.482.0",
35
+ "@genesislcap/ts-builder": "14.482.0",
36
+ "@genesislcap/uvu-playwright-builder": "14.482.0",
37
+ "@genesislcap/vite-builder": "14.482.0",
38
+ "@genesislcap/webpack-builder": "14.482.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "react": "^19.0.0",
42
42
  "react-dom": "^19.0.0"
43
43
  },
44
44
  "dependencies": {
45
- "@genesislcap/foundation-forms": "14.481.2-alpha-829ad82.0",
46
- "@genesislcap/foundation-layout": "14.481.2-alpha-829ad82.0",
47
- "@genesislcap/foundation-logger": "14.481.2-alpha-829ad82.0",
48
- "@genesislcap/web-core": "14.481.2-alpha-829ad82.0",
45
+ "@genesislcap/foundation-forms": "14.482.0",
46
+ "@genesislcap/foundation-layout": "14.482.0",
47
+ "@genesislcap/foundation-logger": "14.482.0",
48
+ "@genesislcap/web-core": "14.482.0",
49
49
  "@jsonforms/core": "^3.2.1",
50
50
  "@r2wc/react-to-web-component": "^2.0.2"
51
51
  },
@@ -58,5 +58,5 @@
58
58
  "access": "public"
59
59
  },
60
60
  "customElements": "dist/custom-elements.json",
61
- "gitHead": "074779b7bd0998af5157821fa5739ecce0ac36a8"
61
+ "gitHead": "176fdab04821da415454fcdbc124f7fabac07aa7"
62
62
  }
@@ -0,0 +1,216 @@
1
+ import { createLogger } from '@genesislcap/foundation-logger';
2
+ import {
3
+ Component as ReactClassComponent,
4
+ memo,
5
+ useEffect,
6
+ useSyncExternalStore,
7
+ type ComponentType,
8
+ type ReactNode,
9
+ } from 'react';
10
+ import { createPortal, flushSync } from 'react-dom';
11
+
12
+ const logger = createLogger('foundation-react-utils');
13
+
14
+ /**
15
+ * A single mounted cell tracked by a {@link GridProCellPortals} manager.
16
+ * @internal
17
+ */
18
+ interface CellPortalEntry {
19
+ id: number;
20
+ container: HTMLElement;
21
+ Component: ComponentType<any>;
22
+ params: any;
23
+ }
24
+
25
+ /**
26
+ * Shared portal manager for Grid Pro React cell renderers.
27
+ *
28
+ * Created with {@link createGridProCellPortals}. Render the {@link GridProCellPortals.Portals}
29
+ * component exactly once inside your React tree — cells rendered through this manager become
30
+ * part of that tree, so they inherit React Context (Redux providers, theme, router, etc.) from
31
+ * wherever `Portals` sits. Pass the manager to {@link createGridProCellRenderer} via the
32
+ * `portals` option to opt a renderer into portal mode.
33
+ *
34
+ * @public
35
+ */
36
+ export interface GridProCellPortals {
37
+ /**
38
+ * Host component for all cells rendered through this manager. Render it exactly once,
39
+ * inside every provider the cell renderers should inherit, and keep it mounted for as
40
+ * long as the grid is on screen — unmounting it blanks all portal-rendered cells.
41
+ */
42
+ Portals: ComponentType;
43
+ /**
44
+ * Mounts a component into a cell container and returns the entry id.
45
+ * Called by the cell renderer wrapper — not intended for direct use.
46
+ * @internal
47
+ */
48
+ mount(container: HTMLElement, Component: ComponentType<any>, params: any): number;
49
+ /**
50
+ * Re-renders a mounted cell with new params.
51
+ * Called by the cell renderer wrapper — not intended for direct use.
52
+ * @internal
53
+ */
54
+ update(id: number, params: any): void;
55
+ /**
56
+ * Removes a mounted cell.
57
+ * Called by the cell renderer wrapper — not intended for direct use.
58
+ * @internal
59
+ */
60
+ remove(id: number): void;
61
+ }
62
+
63
+ /**
64
+ * Error boundary around each portal-rendered cell. A shared React tree means one throwing
65
+ * renderer would otherwise unmount every portal cell in the grid — this contains the blast
66
+ * radius to the failing cell (which renders empty).
67
+ * @internal
68
+ */
69
+ class CellPortalErrorBoundary extends ReactClassComponent<
70
+ { children: ReactNode },
71
+ { failed: boolean }
72
+ > {
73
+ public override state = { failed: false };
74
+
75
+ public static getDerivedStateFromError() {
76
+ return { failed: true };
77
+ }
78
+
79
+ public override componentDidCatch(error: unknown) {
80
+ logger.error('Grid Pro portal cell renderer threw during render:', error);
81
+ }
82
+
83
+ public override render() {
84
+ return this.state.failed ? null : this.props.children;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * One portal per cell. Memoised on the entry object — `update()` swaps the entry, so only
90
+ * the changed cell reconciles when the manager re-renders.
91
+ * @internal
92
+ */
93
+ const CellPortal = memo(function GridProCellPortal({ entry }: { entry: CellPortalEntry }) {
94
+ return createPortal(
95
+ <CellPortalErrorBoundary>
96
+ <entry.Component {...entry.params} />
97
+ </CellPortalErrorBoundary>,
98
+ entry.container,
99
+ );
100
+ });
101
+
102
+ /**
103
+ * Creates a {@link GridProCellPortals} manager: a single React root shared by every cell
104
+ * renderer registered with it, with each cell rendered into its grid cell via a portal.
105
+ *
106
+ * Use this instead of the default one-root-per-cell mode when cell renderers need to
107
+ * inherit React Context from the application tree (Redux, theme, router, query clients...)
108
+ * — portal-rendered cells live in the tree where `Portals` is rendered, so context updates
109
+ * propagate into visible cells like any other React state.
110
+ *
111
+ * @example
112
+ * ```tsx
113
+ * const cellPortals = createGridProCellPortals();
114
+ *
115
+ * const gridOptions = {
116
+ * components: {
117
+ * priceCell: createGridProCellRenderer(PriceCell, { portals: cellPortals }),
118
+ * },
119
+ * columnDefs: [{ field: 'price', cellRenderer: 'priceCell' }],
120
+ * };
121
+ *
122
+ * function Page() {
123
+ * return (
124
+ * <SomeProvider>
125
+ * <cellPortals.Portals />
126
+ * <RapidGridPro gridOptions={gridOptions} />
127
+ * </SomeProvider>
128
+ * );
129
+ * }
130
+ * ```
131
+ *
132
+ * @remarks
133
+ * Trade-offs versus the default per-cell roots: all cells registered with one manager share
134
+ * a React tree, so a renderer that throws is contained by a per-cell error boundary (the
135
+ * failing cell renders empty rather than crashing the rest), and unmounting `Portals` while
136
+ * the grid is alive blanks every portal-rendered cell. Cell mounts are committed
137
+ * synchronously (so AG Grid measures real content), which re-renders the manager once per
138
+ * created cell — the per-cell portals are memoised, keeping that cheap at grid-pro's
139
+ * virtualised cell counts.
140
+ *
141
+ * @public
142
+ */
143
+ export function createGridProCellPortals(): GridProCellPortals {
144
+ let entries: readonly CellPortalEntry[] = [];
145
+ let nextId = 0;
146
+ let portalsMounted = false;
147
+ const listeners = new Set<() => void>();
148
+
149
+ const emit = () => {
150
+ listeners.forEach((listener) => listener());
151
+ };
152
+
153
+ /**
154
+ * Commits a store change synchronously so the cell content exists in the DOM before
155
+ * AG Grid measures the container (autoHeight, autoSizeColumns, sizeColumnsToFit).
156
+ * Mount runs from AG Grid's own (non-React) call stack, so flushing is safe; if it
157
+ * ever runs inside a host React render, React warns and defers the flush instead.
158
+ */
159
+ const emitSync = () => {
160
+ if (!portalsMounted) {
161
+ emit();
162
+ return;
163
+ }
164
+ flushSync(emit);
165
+ };
166
+
167
+ const subscribe = (listener: () => void) => {
168
+ listeners.add(listener);
169
+ return () => {
170
+ listeners.delete(listener);
171
+ };
172
+ };
173
+ const getSnapshot = () => entries;
174
+
175
+ const Portals: ComponentType = () => {
176
+ const snapshot = useSyncExternalStore(subscribe, getSnapshot);
177
+ useEffect(() => {
178
+ portalsMounted = true;
179
+ return () => {
180
+ portalsMounted = false;
181
+ };
182
+ }, []);
183
+ return (
184
+ <>
185
+ {snapshot.map((entry) => (
186
+ <CellPortal key={entry.id} entry={entry} />
187
+ ))}
188
+ </>
189
+ );
190
+ };
191
+
192
+ return {
193
+ Portals,
194
+ mount(container, Component, params) {
195
+ nextId += 1;
196
+ const id = nextId;
197
+ entries = [...entries, { id, container, Component, params }];
198
+ emitSync();
199
+ return id;
200
+ },
201
+ update(id, params) {
202
+ entries = entries.map((entry) => (entry.id === id ? { ...entry, params } : entry));
203
+ // Async on purpose: refresh() has already returned true and AG Grid does not
204
+ // re-measure on refresh, so there is nothing to commit synchronously for.
205
+ emit();
206
+ },
207
+ remove(id) {
208
+ entries = entries.filter((entry) => entry.id !== id);
209
+ // AG Grid can destroy cells synchronously while React is rendering (e.g. React
210
+ // Router unmounting the page hosting the grid); notifying subscribers there would
211
+ // set state mid-render, so defer with a macrotask — the container is already
212
+ // detached by AG Grid, so the portal lingering one tick is invisible.
213
+ setTimeout(emit);
214
+ },
215
+ };
216
+ }