From 32997cc3f5e430a25d9dc0c418b269548d2ec34c Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Thu, 14 Nov 2024 18:28:01 +0400 Subject: [PATCH 01/29] refactor: revert Grid to async rendering --- packages/react-components/src/Grid.tsx | 47 ++++++++++++------- packages/react-components/src/GridColumn.tsx | 41 +++++++++++----- .../react-components/src/GridColumnGroup.tsx | 8 +--- 3 files changed, 63 insertions(+), 33 deletions(-) diff --git a/packages/react-components/src/Grid.tsx b/packages/react-components/src/Grid.tsx index 6bc10e8a..fccdbed3 100644 --- a/packages/react-components/src/Grid.tsx +++ b/packages/react-components/src/Grid.tsx @@ -1,4 +1,4 @@ -import { type ComponentType, type ForwardedRef, forwardRef, type ReactElement, type RefAttributes } from 'react'; +import { type ComponentType, createContext, type ForwardedRef, forwardRef, type ReactElement, type RefAttributes, type RefObject, useEffect, useRef } from 'react'; import { Grid as _Grid, type GridDefaultItem, @@ -7,6 +7,7 @@ import { } from './generated/Grid.js'; import type { GridRowDetailsReactRendererProps } from './renderers/grid.js'; import { useModelRenderer } from './renderers/useModelRenderer.js'; +import useMergedRefs from './utils/useMergedRefs.js'; export * from './generated/Grid.js'; @@ -15,17 +16,31 @@ export type GridProps = Partial, 'rowDetailsRender rowDetailsRenderer?: ComponentType> | null; }>; +type GridContext = { + gridRef: RefObject> +} + +export const GridContext = createContext(null); + function Grid( props: GridProps, ref: ForwardedRef>, ): ReactElement | null { const [portals, rowDetailsRenderer] = useModelRenderer(props.rowDetailsRenderer, { renderSync: true }); + const innerRef = useRef(null); + const finalRef = useMergedRefs(innerRef, ref); + return ( - <_Grid {...props} ref={ref} rowDetailsRenderer={rowDetailsRenderer}> - {props.children} - {portals} - + + <_Grid {...props} ref={finalRef} rowDetailsRenderer={rowDetailsRenderer}> + {props.children} + {portals} + + + ); } @@ -35,14 +50,14 @@ const ForwardedGrid = forwardRef(Grid) as ( export { ForwardedGrid as Grid }; -customElements.whenDefined('vaadin-grid').then(() => { - const gridProto = customElements.get('vaadin-grid')?.prototype; - const originalRecalculateColumnWidths = gridProto?._recalculateColumnWidths; - gridProto._recalculateColumnWidths = function (...args: any[]) { - // Multiple synchronous calls to the renderers using flushSync cause - // some of the renderers to be called asynchronously (see useRenderer.ts). - // To make sure all the column cell content is rendered before recalculating - // the column widths, we need to make _recalculateColumnWidths asynchronous. - queueMicrotask(() => originalRecalculateColumnWidths.call(this, ...args)); - }; -}); +// customElements.whenDefined('vaadin-grid').then(() => { +// const gridProto = customElements.get('vaadin-grid')?.prototype; +// const originalRecalculateColumnWidths = gridProto?._recalculateColumnWidths; +// gridProto._recalculateColumnWidths = function (...args: any[]) { +// // Multiple synchronous calls to the renderers using flushSync cause +// // some of the renderers to be called asynchronously (see useRenderer.ts). +// // To make sure all the column cell content is rendered before recalculating +// // the column widths, we need to make _recalculateColumnWidths asynchronous. +// queueMicrotask(() => originalRecalculateColumnWidths.call(this, ...args)); +// }; +// }); diff --git a/packages/react-components/src/GridColumn.tsx b/packages/react-components/src/GridColumn.tsx index a5979253..bd1a22af 100644 --- a/packages/react-components/src/GridColumn.tsx +++ b/packages/react-components/src/GridColumn.tsx @@ -5,8 +5,11 @@ import { type ReactElement, type ReactNode, type RefAttributes, + useContext, + useEffect, + useRef, } from 'react'; -import type { GridDefaultItem } from './generated/Grid.js'; +import type { GridDefaultItem, GridElement } from './generated/Grid.js'; import { GridColumn as _GridColumn, type GridColumnElement, @@ -15,6 +18,8 @@ import { import type { GridBodyReactRendererProps, GridEdgeReactRendererProps } from './renderers/grid.js'; import { useModelRenderer } from './renderers/useModelRenderer.js'; import { useSimpleOrChildrenRenderer } from './renderers/useSimpleOrChildrenRenderer.js'; +import useMergedRefs from './utils/useMergedRefs.js'; +import { GridContext } from './Grid.js'; export * from './generated/GridColumn.js'; @@ -54,22 +59,36 @@ function GridColumn( { children, footer, header, ...props }: GridColumnProps, ref: ForwardedRef>, ): ReactElement | null { - const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header, { - renderSync: true, - }); - const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer, { - renderSync: true, - }); - const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? children, { - renderSync: true, - }); + const { gridRef } = useContext(GridContext)!; + + const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header); + const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); + const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? children); + + const innerRef = useRef>(null); + + useEffect(() => { + const gridElement = gridRef.current; + const columnElement = innerRef.current; + if (!props.autoWidth || !gridElement || !columnElement || !bodyPortals) { + return; + } + + if (!columnElement.hidden && bodyPortals.length > 0) { + // @ts-ignore + gridElement._recalculateColumnWidths([columnElement]); + // @TODO set a flag to recalculate width only once + } + }, [bodyPortals]); + + const finalRef = useMergedRefs(innerRef, ref); return ( <_GridColumn {...props} footerRenderer={footerRenderer} headerRenderer={headerRenderer} - ref={ref} + ref={finalRef} renderer={bodyRenderer} > {headerPortals} diff --git a/packages/react-components/src/GridColumnGroup.tsx b/packages/react-components/src/GridColumnGroup.tsx index b7518b4e..3bd0eaa3 100644 --- a/packages/react-components/src/GridColumnGroup.tsx +++ b/packages/react-components/src/GridColumnGroup.tsx @@ -40,12 +40,8 @@ function GridColumnGroup( { children, footer, header, ...props }: GridColumnGroupProps, ref: ForwardedRef, ): ReactElement | null { - const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header, { - renderSync: true, - }); - const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer, { - renderSync: true, - }); + const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header); + const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); return ( <_GridColumnGroup {...props} footerRenderer={footerRenderer} headerRenderer={headerRenderer} ref={ref}> From 88c276395a6846a64bf52500ba10acb07c6774da Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Thu, 14 Nov 2024 18:29:14 +0400 Subject: [PATCH 02/29] run formatter --- packages/react-components/src/Grid.tsx | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/react-components/src/Grid.tsx b/packages/react-components/src/Grid.tsx index fccdbed3..56a1cad3 100644 --- a/packages/react-components/src/Grid.tsx +++ b/packages/react-components/src/Grid.tsx @@ -1,4 +1,14 @@ -import { type ComponentType, createContext, type ForwardedRef, forwardRef, type ReactElement, type RefAttributes, type RefObject, useEffect, useRef } from 'react'; +import { + type ComponentType, + createContext, + type ForwardedRef, + forwardRef, + type ReactElement, + type RefAttributes, + type RefObject, + useEffect, + useRef, +} from 'react'; import { Grid as _Grid, type GridDefaultItem, @@ -17,8 +27,8 @@ export type GridProps = Partial, 'rowDetailsRender }>; type GridContext = { - gridRef: RefObject> -} + gridRef: RefObject>; +}; export const GridContext = createContext(null); @@ -32,15 +42,16 @@ function Grid( const finalRef = useMergedRefs(innerRef, ref); return ( - + <_Grid {...props} ref={finalRef} rowDetailsRenderer={rowDetailsRenderer}> {props.children} {portals} - ); } From 391ea109ac866f79f4d1474d173e6024a2cdd007 Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Fri, 15 Nov 2024 11:56:58 +0400 Subject: [PATCH 03/29] update implementation --- packages/react-components/src/Grid.tsx | 51 +++++++++---------- packages/react-components/src/GridColumn.tsx | 31 ++++------- .../react-components/src/GridColumnGroup.tsx | 20 ++++++-- .../src/renderers/useRenderer.ts | 2 + .../src/utils/markElementAsRendered.ts | 9 ++++ 5 files changed, 61 insertions(+), 52 deletions(-) create mode 100644 packages/react-components/src/utils/markElementAsRendered.ts diff --git a/packages/react-components/src/Grid.tsx b/packages/react-components/src/Grid.tsx index 56a1cad3..7fc1f2d6 100644 --- a/packages/react-components/src/Grid.tsx +++ b/packages/react-components/src/Grid.tsx @@ -1,6 +1,5 @@ import { type ComponentType, - createContext, type ForwardedRef, forwardRef, type ReactElement, @@ -18,6 +17,7 @@ import { import type { GridRowDetailsReactRendererProps } from './renderers/grid.js'; import { useModelRenderer } from './renderers/useModelRenderer.js'; import useMergedRefs from './utils/useMergedRefs.js'; +import { isElementMarkedAsRendered } from './utils/markElementAsRendered.js'; export * from './generated/Grid.js'; @@ -26,32 +26,39 @@ export type GridProps = Partial, 'rowDetailsRender rowDetailsRenderer?: ComponentType> | null; }>; -type GridContext = { - gridRef: RefObject>; -}; +function overrideRecalculateColumnWidths(grid: GridElement) { + const originalRecalculateColumnWidths = grid.recalculateColumnWidths; + grid.recalculateColumnWidths = function (...args) { + originalRecalculateColumnWidths.apply(this, args); -export const GridContext = createContext(null); + // @ts-ignore + if (this._getColumns().some((column) => !column.hidden && !isElementMarkedAsRendered(column))) { + // @ts-ignore + this.__pendingRecalculateColumnWidths = true; + } + }; +} function Grid( props: GridProps, ref: ForwardedRef>, ): ReactElement | null { - const [portals, rowDetailsRenderer] = useModelRenderer(props.rowDetailsRenderer, { renderSync: true }); + const [portals, rowDetailsRenderer] = useModelRenderer(props.rowDetailsRenderer); const innerRef = useRef(null); const finalRef = useMergedRefs(innerRef, ref); + useEffect(() => { + if (innerRef.current) { + overrideRecalculateColumnWidths(innerRef.current); + } + }, [innerRef.current]); + return ( - - <_Grid {...props} ref={finalRef} rowDetailsRenderer={rowDetailsRenderer}> - {props.children} - {portals} - - + <_Grid {...props} ref={finalRef} rowDetailsRenderer={rowDetailsRenderer}> + {props.children} + {portals} + ); } @@ -60,15 +67,3 @@ const ForwardedGrid = forwardRef(Grid) as ( ) => ReactElement | null; export { ForwardedGrid as Grid }; - -// customElements.whenDefined('vaadin-grid').then(() => { -// const gridProto = customElements.get('vaadin-grid')?.prototype; -// const originalRecalculateColumnWidths = gridProto?._recalculateColumnWidths; -// gridProto._recalculateColumnWidths = function (...args: any[]) { -// // Multiple synchronous calls to the renderers using flushSync cause -// // some of the renderers to be called asynchronously (see useRenderer.ts). -// // To make sure all the column cell content is rendered before recalculating -// // the column widths, we need to make _recalculateColumnWidths asynchronous. -// queueMicrotask(() => originalRecalculateColumnWidths.call(this, ...args)); -// }; -// }); diff --git a/packages/react-components/src/GridColumn.tsx b/packages/react-components/src/GridColumn.tsx index bd1a22af..5d1a4548 100644 --- a/packages/react-components/src/GridColumn.tsx +++ b/packages/react-components/src/GridColumn.tsx @@ -5,11 +5,10 @@ import { type ReactElement, type ReactNode, type RefAttributes, - useContext, useEffect, useRef, } from 'react'; -import type { GridDefaultItem, GridElement } from './generated/Grid.js'; +import type { GridDefaultItem } from './generated/Grid.js'; import { GridColumn as _GridColumn, type GridColumnElement, @@ -19,7 +18,7 @@ import type { GridBodyReactRendererProps, GridEdgeReactRendererProps } from './r import { useModelRenderer } from './renderers/useModelRenderer.js'; import { useSimpleOrChildrenRenderer } from './renderers/useSimpleOrChildrenRenderer.js'; import useMergedRefs from './utils/useMergedRefs.js'; -import { GridContext } from './Grid.js'; +import { markElementAsRendered } from './utils/markElementAsRendered.js'; export * from './generated/GridColumn.js'; @@ -59,29 +58,19 @@ function GridColumn( { children, footer, header, ...props }: GridColumnProps, ref: ForwardedRef>, ): ReactElement | null { - const { gridRef } = useContext(GridContext)!; - - const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header); - const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); - const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? children); + const [headerPortals, headerRenderer, isHeaderRendered] = useSimpleOrChildrenRenderer(props.headerRenderer, header); + const [footerPortals, footerRenderer, isFooterRendered] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); + const [bodyPortals, bodyRenderer, isBodyRendered] = useModelRenderer(props.renderer ?? children); + const isRendered = !!(isHeaderRendered && isFooterRendered && isBodyRendered); const innerRef = useRef>(null); + const finalRef = useMergedRefs(innerRef, ref); useEffect(() => { - const gridElement = gridRef.current; - const columnElement = innerRef.current; - if (!props.autoWidth || !gridElement || !columnElement || !bodyPortals) { - return; - } - - if (!columnElement.hidden && bodyPortals.length > 0) { - // @ts-ignore - gridElement._recalculateColumnWidths([columnElement]); - // @TODO set a flag to recalculate width only once + if (innerRef.current && isRendered) { + markElementAsRendered(innerRef.current); } - }, [bodyPortals]); - - const finalRef = useMergedRefs(innerRef, ref); + }, [innerRef.current, isRendered]); return ( <_GridColumn diff --git a/packages/react-components/src/GridColumnGroup.tsx b/packages/react-components/src/GridColumnGroup.tsx index 3bd0eaa3..fac22f01 100644 --- a/packages/react-components/src/GridColumnGroup.tsx +++ b/packages/react-components/src/GridColumnGroup.tsx @@ -1,5 +1,7 @@ import { forwardRef, + useEffect, + useRef, type ComponentType, type ForwardedRef, type ReactElement, @@ -14,6 +16,8 @@ import { import { type ReactSimpleRendererProps } from './renderers/useSimpleRenderer.js'; import { useSimpleOrChildrenRenderer } from './renderers/useSimpleOrChildrenRenderer.js'; import type { OmittedGridColumnHTMLAttributes } from './GridColumn.js'; +import useMergedRefs from './utils/useMergedRefs.js'; +import { markElementAsRendered } from './utils/markElementAsRendered.js'; export * from './generated/GridColumnGroup.js'; @@ -40,11 +44,21 @@ function GridColumnGroup( { children, footer, header, ...props }: GridColumnGroupProps, ref: ForwardedRef, ): ReactElement | null { - const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header); - const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); + const [headerPortals, headerRenderer, isHeaderRendered] = useSimpleOrChildrenRenderer(props.headerRenderer, header); + const [footerPortals, footerRenderer, isFooterRendered] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); + const isRendered = !!(isHeaderRendered && isFooterRendered); + + const innerRef = useRef(null); + const finalRef = useMergedRefs(innerRef, ref); + + useEffect(() => { + if (innerRef.current && isRendered) { + markElementAsRendered(innerRef.current); + } + }, [innerRef.current, isRendered]); return ( - <_GridColumnGroup {...props} footerRenderer={footerRenderer} headerRenderer={headerRenderer} ref={ref}> + <_GridColumnGroup {...props} footerRenderer={footerRenderer} headerRenderer={headerRenderer} ref={finalRef}> {headerPortals} {footerPortals} {children} diff --git a/packages/react-components/src/renderers/useRenderer.ts b/packages/react-components/src/renderers/useRenderer.ts index 79bb02b3..77bb1949 100644 --- a/packages/react-components/src/renderers/useRenderer.ts +++ b/packages/react-components/src/renderers/useRenderer.ts @@ -13,6 +13,7 @@ import type { Slice, WebComponentRenderer } from './renderer.js'; export type UseRendererResult = readonly [ portals?: ReadonlyArray, renderer?: W, + isRendered?: boolean, ]; const initialState = new Map(); @@ -79,6 +80,7 @@ export function useRenderer

( ), ), renderer, + map.size > 0 ] : []; } diff --git a/packages/react-components/src/utils/markElementAsRendered.ts b/packages/react-components/src/utils/markElementAsRendered.ts new file mode 100644 index 00000000..6fb0fbaf --- /dev/null +++ b/packages/react-components/src/utils/markElementAsRendered.ts @@ -0,0 +1,9 @@ +const ELEMENT_RENDERED = Symbol(); + +export function markElementAsRendered(element: HTMLElement) { + element[ELEMENT_RENDERED] = true; +} + +export function isElementMarkedAsRendered(element: HTMLElement) { + return element[ELEMENT_RENDERED]; +} From cadc3c8e6824c8ee31e0800169a2dc727aa4ce26 Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Fri, 15 Nov 2024 14:11:24 +0400 Subject: [PATCH 04/29] use different approach --- packages/react-components/src/Grid.tsx | 53 ++++++++++++------- packages/react-components/src/GridColumn.tsx | 14 +++-- .../react-components/src/GridColumnGroup.tsx | 12 +++-- .../react-components/src/GridFilterColumn.tsx | 27 +++++++--- .../src/GridSelectionColumn.tsx | 12 ++--- .../react-components/src/GridSortColumn.tsx | 8 +-- .../react-components/src/GridTreeColumn.tsx | 8 +-- .../src/utils/markElementAsRendered.ts | 6 +-- 8 files changed, 80 insertions(+), 60 deletions(-) diff --git a/packages/react-components/src/Grid.tsx b/packages/react-components/src/Grid.tsx index 7fc1f2d6..2412ac74 100644 --- a/packages/react-components/src/Grid.tsx +++ b/packages/react-components/src/Grid.tsx @@ -1,10 +1,12 @@ import { type ComponentType, + createContext, type ForwardedRef, forwardRef, type ReactElement, type RefAttributes, type RefObject, + useCallback, useEffect, useRef, } from 'react'; @@ -17,7 +19,7 @@ import { import type { GridRowDetailsReactRendererProps } from './renderers/grid.js'; import { useModelRenderer } from './renderers/useModelRenderer.js'; import useMergedRefs from './utils/useMergedRefs.js'; -import { isElementMarkedAsRendered } from './utils/markElementAsRendered.js'; +import { isElementMarkedAsRendered, markElementAsRendered } from './utils/markElementAsRendered.js'; export * from './generated/Grid.js'; @@ -26,18 +28,11 @@ export type GridProps = Partial, 'rowDetailsRender rowDetailsRenderer?: ComponentType> | null; }>; -function overrideRecalculateColumnWidths(grid: GridElement) { - const originalRecalculateColumnWidths = grid.recalculateColumnWidths; - grid.recalculateColumnWidths = function (...args) { - originalRecalculateColumnWidths.apply(this, args); +type GridContext = { + onColumnRendered(column: HTMLElement): void; +}; - // @ts-ignore - if (this._getColumns().some((column) => !column.hidden && !isElementMarkedAsRendered(column))) { - // @ts-ignore - this.__pendingRecalculateColumnWidths = true; - } - }; -} +export const GridContext = createContext(null); function Grid( props: GridProps, @@ -49,16 +44,34 @@ function Grid( const finalRef = useMergedRefs(innerRef, ref); useEffect(() => { - if (innerRef.current) { - overrideRecalculateColumnWidths(innerRef.current); - } - }, [innerRef.current]); + innerRef.current!.recalculateColumnWidths = function (...args) { + // @ts-ignore + const autoWidthColumns: HTMLElement[] = this._getColumns().filter((col) => col.autoWidth && !col.hidden); + if (autoWidthColumns.some((col) => !isElementMarkedAsRendered(col))) { + // @ts-ignore + this.__pendingRecalculateColumnWidths = true; + return; + } + + // console.log('rendered'); + + Object.getPrototypeOf(this).recalculateColumnWidths.apply(this, args); + }; + }, []); + + const onColumnRendered = useCallback((column: HTMLElement) => { + markElementAsRendered(column); + // @ts-ignore + innerRef.current!.__tryToRecalculateColumnWidthsIfPending(); + }, []); return ( - <_Grid {...props} ref={finalRef} rowDetailsRenderer={rowDetailsRenderer}> - {props.children} - {portals} - + + <_Grid {...props} ref={finalRef} rowDetailsRenderer={rowDetailsRenderer}> + {props.children} + {portals} + + ); } diff --git a/packages/react-components/src/GridColumn.tsx b/packages/react-components/src/GridColumn.tsx index 5d1a4548..4c95a082 100644 --- a/packages/react-components/src/GridColumn.tsx +++ b/packages/react-components/src/GridColumn.tsx @@ -5,6 +5,7 @@ import { type ReactElement, type ReactNode, type RefAttributes, + useContext, useEffect, useRef, } from 'react'; @@ -18,7 +19,7 @@ import type { GridBodyReactRendererProps, GridEdgeReactRendererProps } from './r import { useModelRenderer } from './renderers/useModelRenderer.js'; import { useSimpleOrChildrenRenderer } from './renderers/useSimpleOrChildrenRenderer.js'; import useMergedRefs from './utils/useMergedRefs.js'; -import { markElementAsRendered } from './utils/markElementAsRendered.js'; +import { GridContext } from './Grid.js'; export * from './generated/GridColumn.js'; @@ -58,19 +59,22 @@ function GridColumn( { children, footer, header, ...props }: GridColumnProps, ref: ForwardedRef>, ): ReactElement | null { + const gridContext = useContext(GridContext)!; + const [headerPortals, headerRenderer, isHeaderRendered] = useSimpleOrChildrenRenderer(props.headerRenderer, header); const [footerPortals, footerRenderer, isFooterRendered] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); const [bodyPortals, bodyRenderer, isBodyRendered] = useModelRenderer(props.renderer ?? children); - const isRendered = !!(isHeaderRendered && isFooterRendered && isBodyRendered); + const isRendered = + (!headerRenderer || isHeaderRendered) && (!footerRenderer || isFooterRendered) && (!bodyRenderer || isBodyRendered); const innerRef = useRef>(null); const finalRef = useMergedRefs(innerRef, ref); useEffect(() => { - if (innerRef.current && isRendered) { - markElementAsRendered(innerRef.current); + if (isRendered) { + gridContext.onColumnRendered(innerRef.current!); } - }, [innerRef.current, isRendered]); + }, [isRendered]); return ( <_GridColumn diff --git a/packages/react-components/src/GridColumnGroup.tsx b/packages/react-components/src/GridColumnGroup.tsx index fac22f01..a9067ba2 100644 --- a/packages/react-components/src/GridColumnGroup.tsx +++ b/packages/react-components/src/GridColumnGroup.tsx @@ -1,5 +1,6 @@ import { forwardRef, + useContext, useEffect, useRef, type ComponentType, @@ -18,6 +19,7 @@ import { useSimpleOrChildrenRenderer } from './renderers/useSimpleOrChildrenRend import type { OmittedGridColumnHTMLAttributes } from './GridColumn.js'; import useMergedRefs from './utils/useMergedRefs.js'; import { markElementAsRendered } from './utils/markElementAsRendered.js'; +import { GridContext } from './Grid.js'; export * from './generated/GridColumnGroup.js'; @@ -44,18 +46,20 @@ function GridColumnGroup( { children, footer, header, ...props }: GridColumnGroupProps, ref: ForwardedRef, ): ReactElement | null { + const gridContext = useContext(GridContext)!; + const [headerPortals, headerRenderer, isHeaderRendered] = useSimpleOrChildrenRenderer(props.headerRenderer, header); const [footerPortals, footerRenderer, isFooterRendered] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); - const isRendered = !!(isHeaderRendered && isFooterRendered); + const isRendered = (!headerRenderer || isHeaderRendered) && (!footerRenderer || isFooterRendered); const innerRef = useRef(null); const finalRef = useMergedRefs(innerRef, ref); useEffect(() => { - if (innerRef.current && isRendered) { - markElementAsRendered(innerRef.current); + if (isRendered) { + gridContext.onColumnRendered(innerRef.current!); } - }, [innerRef.current, isRendered]); + }, [isRendered]); return ( <_GridColumnGroup {...props} footerRenderer={footerRenderer} headerRenderer={headerRenderer} ref={finalRef}> diff --git a/packages/react-components/src/GridFilterColumn.tsx b/packages/react-components/src/GridFilterColumn.tsx index 8489a86c..7d66a48a 100644 --- a/packages/react-components/src/GridFilterColumn.tsx +++ b/packages/react-components/src/GridFilterColumn.tsx @@ -5,6 +5,9 @@ import { type ReactElement, type ReactNode, type RefAttributes, + useContext, + useEffect, + useRef, } from 'react'; import type { GridDefaultItem } from './generated/Grid.js'; import { @@ -16,6 +19,8 @@ import type { GridBodyReactRendererProps, GridEdgeReactRendererProps } from './r import { useModelRenderer } from './renderers/useModelRenderer.js'; import { useSimpleOrChildrenRenderer } from './renderers/useSimpleOrChildrenRenderer.js'; import type { OmittedGridColumnHTMLAttributes } from './GridColumn.js'; +import { GridContext } from './Grid.js'; +import useMergedRefs from './utils/useMergedRefs.js'; export * from './generated/GridFilterColumn.js'; @@ -43,15 +48,23 @@ function GridFilterColumn( { footer, ...props }: GridFilterColumnProps, ref: ForwardedRef>, ): ReactElement | null { - const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer, { - renderSync: true, - }); - const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? props.children, { - renderSync: true, - }); + const gridContext = useContext(GridContext)!; + + const [footerPortals, footerRenderer, isFooterRendered] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); + const [bodyPortals, bodyRenderer, isBodyRendered] = useModelRenderer(props.renderer ?? props.children); + const isRendered = (!footerRenderer || isFooterRendered) && (!bodyRenderer || isBodyRendered); + + const innerRef = useRef>(null); + const finalRef = useMergedRefs(innerRef, ref); + + useEffect(() => { + if (isRendered) { + gridContext.onColumnRendered(innerRef.current!); + } + }, [isRendered]); return ( - <_GridFilterColumn {...props} footerRenderer={footerRenderer} ref={ref} renderer={bodyRenderer}> + <_GridFilterColumn {...props} footerRenderer={footerRenderer} ref={finalRef} renderer={bodyRenderer}> {footerPortals} {bodyPortals} diff --git a/packages/react-components/src/GridSelectionColumn.tsx b/packages/react-components/src/GridSelectionColumn.tsx index df3913c0..223b6dcf 100644 --- a/packages/react-components/src/GridSelectionColumn.tsx +++ b/packages/react-components/src/GridSelectionColumn.tsx @@ -49,15 +49,9 @@ function GridSelectionColumn( { footer, header, ...props }: GridSelectionColumnProps, ref: ForwardedRef>, ): ReactElement | null { - const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header, { - renderSync: true, - }); - const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer, { - renderSync: true, - }); - const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? props.children, { - renderSync: true, - }); + const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header); + const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); + const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? props.children); return ( <_GridSelectionColumn diff --git a/packages/react-components/src/GridSortColumn.tsx b/packages/react-components/src/GridSortColumn.tsx index 3b405a40..d460c527 100644 --- a/packages/react-components/src/GridSortColumn.tsx +++ b/packages/react-components/src/GridSortColumn.tsx @@ -42,12 +42,8 @@ function GridSortColumn( { footer, ...props }: GridSortColumnProps, ref: ForwardedRef>, ): ReactElement | null { - const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer, { - renderSync: true, - }); - const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? props.children, { - renderSync: true, - }); + const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); + const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? props.children); return ( <_GridSortColumn {...props} footerRenderer={footerRenderer} ref={ref} renderer={bodyRenderer}> diff --git a/packages/react-components/src/GridTreeColumn.tsx b/packages/react-components/src/GridTreeColumn.tsx index 85f5aafa..fcd9cedf 100644 --- a/packages/react-components/src/GridTreeColumn.tsx +++ b/packages/react-components/src/GridTreeColumn.tsx @@ -46,12 +46,8 @@ function GridTreeColumn( { footer, header, ...props }: GridTreeColumnProps, ref: ForwardedRef>, ): ReactElement | null { - const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header, { - renderSync: true, - }); - const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer, { - renderSync: true, - }); + const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header); + const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); return ( <_GridTreeColumn {...props} headerRenderer={headerRenderer} footerRenderer={footerRenderer} ref={ref}> diff --git a/packages/react-components/src/utils/markElementAsRendered.ts b/packages/react-components/src/utils/markElementAsRendered.ts index 6fb0fbaf..94846977 100644 --- a/packages/react-components/src/utils/markElementAsRendered.ts +++ b/packages/react-components/src/utils/markElementAsRendered.ts @@ -1,9 +1,9 @@ const ELEMENT_RENDERED = Symbol(); -export function markElementAsRendered(element: HTMLElement) { +export function markElementAsRendered(element: HTMLElement & { [ELEMENT_RENDERED]?: boolean }) { element[ELEMENT_RENDERED] = true; } -export function isElementMarkedAsRendered(element: HTMLElement) { - return element[ELEMENT_RENDERED]; +export function isElementMarkedAsRendered(element: HTMLElement & { [ELEMENT_RENDERED]?: boolean }) { + return !!element[ELEMENT_RENDERED]; } From 2a25b007d1c5958c3abbe0cf1ee0c634b0564b4a Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Fri, 15 Nov 2024 16:34:08 +0400 Subject: [PATCH 05/29] update implementation --- packages/react-components/src/Grid.tsx | 92 ++++++++++++++----- packages/react-components/src/GridColumn.tsx | 24 ++--- .../react-components/src/GridColumnGroup.tsx | 17 +--- .../react-components/src/GridFilterColumn.tsx | 18 +--- .../src/GridSelectionColumn.tsx | 14 ++- .../react-components/src/GridSortColumn.tsx | 11 ++- .../react-components/src/GridTreeColumn.tsx | 11 ++- .../src/renderers/useRenderer.ts | 2 - 8 files changed, 122 insertions(+), 67 deletions(-) diff --git a/packages/react-components/src/Grid.tsx b/packages/react-components/src/Grid.tsx index 2412ac74..94a02c34 100644 --- a/packages/react-components/src/Grid.tsx +++ b/packages/react-components/src/Grid.tsx @@ -7,8 +7,10 @@ import { type RefAttributes, type RefObject, useCallback, + useContext, useEffect, useRef, + useState, } from 'react'; import { Grid as _Grid, @@ -19,7 +21,12 @@ import { import type { GridRowDetailsReactRendererProps } from './renderers/grid.js'; import { useModelRenderer } from './renderers/useModelRenderer.js'; import useMergedRefs from './utils/useMergedRefs.js'; -import { isElementMarkedAsRendered, markElementAsRendered } from './utils/markElementAsRendered.js'; +import type { GridColumnElement } from './GridColumn.js'; +import type { GridSelectionColumnElement } from './GridSelectionColumn.js'; +import type { GridFilterColumnElement } from './GridFilterColumn.js'; +import type { GridSortColumnElement } from './GridSortColumn.js'; +import type { GridTreeColumnElement } from './GridTreeColumn.js'; +import { GridColumnGroupElement } from './GridColumnGroup.js'; export * from './generated/Grid.js'; @@ -28,11 +35,39 @@ export type GridProps = Partial, 'rowDetailsRender rowDetailsRenderer?: ComponentType> | null; }>; +type AnyGridColumnElement = + | GridColumnElement + | GridSelectionColumnElement + | GridFilterColumnElement + | GridSortColumnElement + | GridTreeColumnElement + | GridColumnGroupElement; + type GridContext = { - onColumnRendered(column: HTMLElement): void; + onColumnAdded(column: AnyGridColumnElement): void; + onColumnRemoved(column: AnyGridColumnElement): void; + onColumnRendered(column: AnyGridColumnElement): void; }; -export const GridContext = createContext(null); +const GridContext = createContext(null); + +export function useGridColumn(columnRef: RefObject, isRendered: boolean) { + const gridContext = useContext(GridContext)!; + + useEffect(() => { + gridContext.onColumnAdded(columnRef.current!); + + return () => { + gridContext.onColumnRemoved(columnRef.current!); + }; + }, []); + + useEffect(() => { + if (isRendered) { + gridContext.onColumnRendered(columnRef.current!); + } + }, [isRendered]); +} function Grid( props: GridProps, @@ -43,30 +78,43 @@ function Grid( const innerRef = useRef(null); const finalRef = useMergedRefs(innerRef, ref); - useEffect(() => { - innerRef.current!.recalculateColumnWidths = function (...args) { - // @ts-ignore - const autoWidthColumns: HTMLElement[] = this._getColumns().filter((col) => col.autoWidth && !col.hidden); - if (autoWidthColumns.some((col) => !isElementMarkedAsRendered(col))) { - // @ts-ignore - this.__pendingRecalculateColumnWidths = true; - return; - } - - // console.log('rendered'); - - Object.getPrototypeOf(this).recalculateColumnWidths.apply(this, args); - }; + const columnsRef = useRef>(new Set()); + const [renderedColumns, setRenderedColumns] = useState>(new Set()); + const [isColumnsWidthRecalculationPending, setColumnsWidthRecalculationPending] = useState(true); + + const onColumnAdded = useCallback((column: AnyGridColumnElement) => { + columnsRef.current.add(column); }, []); - const onColumnRendered = useCallback((column: HTMLElement) => { - markElementAsRendered(column); - // @ts-ignore - innerRef.current!.__tryToRecalculateColumnWidthsIfPending(); + const onColumnRemoved = useCallback((column: AnyGridColumnElement) => { + columnsRef.current.delete(column); + + setRenderedColumns((columns) => { + columns.delete(column); + return new Set(columns); + }); }, []); + const onColumnRendered = useCallback((column: AnyGridColumnElement) => { + setRenderedColumns((columns) => { + columns.add(column); + return new Set(columns); + }); + }, []); + + useEffect(() => { + if (!isColumnsWidthRecalculationPending || columnsRef.current.size === 0) { + return; + } + + if ([...columnsRef.current].every((col) => col.hidden || renderedColumns.has(col))) { + innerRef.current!.recalculateColumnWidths(); + setColumnsWidthRecalculationPending(false); + } + }, [renderedColumns, isColumnsWidthRecalculationPending]); + return ( - + <_Grid {...props} ref={finalRef} rowDetailsRenderer={rowDetailsRenderer}> {props.children} {portals} diff --git a/packages/react-components/src/GridColumn.tsx b/packages/react-components/src/GridColumn.tsx index 4c95a082..0b0c1584 100644 --- a/packages/react-components/src/GridColumn.tsx +++ b/packages/react-components/src/GridColumn.tsx @@ -5,8 +5,6 @@ import { type ReactElement, type ReactNode, type RefAttributes, - useContext, - useEffect, useRef, } from 'react'; import type { GridDefaultItem } from './generated/Grid.js'; @@ -19,7 +17,7 @@ import type { GridBodyReactRendererProps, GridEdgeReactRendererProps } from './r import { useModelRenderer } from './renderers/useModelRenderer.js'; import { useSimpleOrChildrenRenderer } from './renderers/useSimpleOrChildrenRenderer.js'; import useMergedRefs from './utils/useMergedRefs.js'; -import { GridContext } from './Grid.js'; +import { useGridColumn } from './Grid.js'; export * from './generated/GridColumn.js'; @@ -59,22 +57,18 @@ function GridColumn( { children, footer, header, ...props }: GridColumnProps, ref: ForwardedRef>, ): ReactElement | null { - const gridContext = useContext(GridContext)!; - - const [headerPortals, headerRenderer, isHeaderRendered] = useSimpleOrChildrenRenderer(props.headerRenderer, header); - const [footerPortals, footerRenderer, isFooterRendered] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); - const [bodyPortals, bodyRenderer, isBodyRendered] = useModelRenderer(props.renderer ?? children); - const isRendered = - (!headerRenderer || isHeaderRendered) && (!footerRenderer || isFooterRendered) && (!bodyRenderer || isBodyRendered); + const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header); + const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); + const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? children); const innerRef = useRef>(null); const finalRef = useMergedRefs(innerRef, ref); - useEffect(() => { - if (isRendered) { - gridContext.onColumnRendered(innerRef.current!); - } - }, [isRendered]); + const isRendered = + (!headerRenderer || headerPortals!.length > 0) && + (!footerRenderer || footerPortals!.length > 0) && + (!bodyRenderer || bodyPortals!.length > 0); + useGridColumn(innerRef, isRendered); return ( <_GridColumn diff --git a/packages/react-components/src/GridColumnGroup.tsx b/packages/react-components/src/GridColumnGroup.tsx index a9067ba2..bb031ec2 100644 --- a/packages/react-components/src/GridColumnGroup.tsx +++ b/packages/react-components/src/GridColumnGroup.tsx @@ -18,8 +18,7 @@ import { type ReactSimpleRendererProps } from './renderers/useSimpleRenderer.js' import { useSimpleOrChildrenRenderer } from './renderers/useSimpleOrChildrenRenderer.js'; import type { OmittedGridColumnHTMLAttributes } from './GridColumn.js'; import useMergedRefs from './utils/useMergedRefs.js'; -import { markElementAsRendered } from './utils/markElementAsRendered.js'; -import { GridContext } from './Grid.js'; +import { useGridColumn } from './Grid.js'; export * from './generated/GridColumnGroup.js'; @@ -46,20 +45,14 @@ function GridColumnGroup( { children, footer, header, ...props }: GridColumnGroupProps, ref: ForwardedRef, ): ReactElement | null { - const gridContext = useContext(GridContext)!; - - const [headerPortals, headerRenderer, isHeaderRendered] = useSimpleOrChildrenRenderer(props.headerRenderer, header); - const [footerPortals, footerRenderer, isFooterRendered] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); - const isRendered = (!headerRenderer || isHeaderRendered) && (!footerRenderer || isFooterRendered); + const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header); + const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); const innerRef = useRef(null); const finalRef = useMergedRefs(innerRef, ref); - useEffect(() => { - if (isRendered) { - gridContext.onColumnRendered(innerRef.current!); - } - }, [isRendered]); + const isRendered = (!headerRenderer || headerPortals!.length > 0) && (!footerRenderer || footerPortals!.length > 0); + useGridColumn(innerRef, isRendered); return ( <_GridColumnGroup {...props} footerRenderer={footerRenderer} headerRenderer={headerRenderer} ref={finalRef}> diff --git a/packages/react-components/src/GridFilterColumn.tsx b/packages/react-components/src/GridFilterColumn.tsx index 7d66a48a..3bd1d7f1 100644 --- a/packages/react-components/src/GridFilterColumn.tsx +++ b/packages/react-components/src/GridFilterColumn.tsx @@ -5,8 +5,6 @@ import { type ReactElement, type ReactNode, type RefAttributes, - useContext, - useEffect, useRef, } from 'react'; import type { GridDefaultItem } from './generated/Grid.js'; @@ -19,8 +17,8 @@ import type { GridBodyReactRendererProps, GridEdgeReactRendererProps } from './r import { useModelRenderer } from './renderers/useModelRenderer.js'; import { useSimpleOrChildrenRenderer } from './renderers/useSimpleOrChildrenRenderer.js'; import type { OmittedGridColumnHTMLAttributes } from './GridColumn.js'; -import { GridContext } from './Grid.js'; import useMergedRefs from './utils/useMergedRefs.js'; +import { useGridColumn } from './Grid.js'; export * from './generated/GridFilterColumn.js'; @@ -48,20 +46,14 @@ function GridFilterColumn( { footer, ...props }: GridFilterColumnProps, ref: ForwardedRef>, ): ReactElement | null { - const gridContext = useContext(GridContext)!; - - const [footerPortals, footerRenderer, isFooterRendered] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); - const [bodyPortals, bodyRenderer, isBodyRendered] = useModelRenderer(props.renderer ?? props.children); - const isRendered = (!footerRenderer || isFooterRendered) && (!bodyRenderer || isBodyRendered); + const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); + const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? props.children); const innerRef = useRef>(null); const finalRef = useMergedRefs(innerRef, ref); - useEffect(() => { - if (isRendered) { - gridContext.onColumnRendered(innerRef.current!); - } - }, [isRendered]); + const isRendered = (!footerRenderer || footerPortals!.length > 0) && (!bodyRenderer || bodyPortals!.length > 0); + useGridColumn(innerRef, isRendered); return ( <_GridFilterColumn {...props} footerRenderer={footerRenderer} ref={finalRef} renderer={bodyRenderer}> diff --git a/packages/react-components/src/GridSelectionColumn.tsx b/packages/react-components/src/GridSelectionColumn.tsx index 223b6dcf..c07573d7 100644 --- a/packages/react-components/src/GridSelectionColumn.tsx +++ b/packages/react-components/src/GridSelectionColumn.tsx @@ -5,6 +5,7 @@ import { type ReactElement, type ReactNode, type RefAttributes, + useRef, } from 'react'; import type { GridDefaultItem } from './generated/Grid.js'; import { @@ -16,6 +17,8 @@ import type { GridBodyReactRendererProps, GridEdgeReactRendererProps } from './r import { useModelRenderer } from './renderers/useModelRenderer.js'; import { useSimpleOrChildrenRenderer } from './renderers/useSimpleOrChildrenRenderer.js'; import type { OmittedGridColumnHTMLAttributes } from './GridColumn.js'; +import { useGridColumn } from './Grid.js'; +import useMergedRefs from './utils/useMergedRefs.js'; export * from './generated/GridSelectionColumn.js'; @@ -53,12 +56,21 @@ function GridSelectionColumn( const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? props.children); + const innerRef = useRef>(null); + const finalRef = useMergedRefs(innerRef, ref); + + const isRendered = + (!headerRenderer || headerPortals!.length > 0) && + (!footerRenderer || footerPortals!.length > 0) && + (!bodyRenderer || bodyPortals!.length > 0); + useGridColumn(innerRef, isRendered); + return ( <_GridSelectionColumn {...props} footerRenderer={footerRenderer} headerRenderer={headerRenderer} - ref={ref} + ref={finalRef} renderer={bodyRenderer} > {headerPortals} diff --git a/packages/react-components/src/GridSortColumn.tsx b/packages/react-components/src/GridSortColumn.tsx index d460c527..e29fb542 100644 --- a/packages/react-components/src/GridSortColumn.tsx +++ b/packages/react-components/src/GridSortColumn.tsx @@ -5,6 +5,7 @@ import { type ReactElement, type ReactNode, type RefAttributes, + useRef, } from 'react'; import type { GridDefaultItem } from './generated/Grid.js'; import { @@ -16,6 +17,8 @@ import type { GridBodyReactRendererProps, GridEdgeReactRendererProps } from './r import { useModelRenderer } from './renderers/useModelRenderer.js'; import { useSimpleOrChildrenRenderer } from './renderers/useSimpleOrChildrenRenderer.js'; import type { OmittedGridColumnHTMLAttributes } from './GridColumn.js'; +import useMergedRefs from './utils/useMergedRefs.js'; +import { useGridColumn } from './Grid.js'; export * from './generated/GridSortColumn.js'; @@ -45,8 +48,14 @@ function GridSortColumn( const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? props.children); + const innerRef = useRef>(null); + const finalRef = useMergedRefs(innerRef, ref); + + const isRendered = (!footerRenderer || footerPortals!.length > 0) && (!bodyRenderer || bodyPortals!.length > 0); + useGridColumn(innerRef, isRendered); + return ( - <_GridSortColumn {...props} footerRenderer={footerRenderer} ref={ref} renderer={bodyRenderer}> + <_GridSortColumn {...props} footerRenderer={footerRenderer} ref={finalRef} renderer={bodyRenderer}> {footerPortals} {bodyPortals} diff --git a/packages/react-components/src/GridTreeColumn.tsx b/packages/react-components/src/GridTreeColumn.tsx index fcd9cedf..95d0b84b 100644 --- a/packages/react-components/src/GridTreeColumn.tsx +++ b/packages/react-components/src/GridTreeColumn.tsx @@ -5,6 +5,7 @@ import { type ReactElement, type ReactNode, type RefAttributes, + useRef, } from 'react'; import type { GridDefaultItem } from './generated/Grid.js'; import { @@ -15,6 +16,8 @@ import { import type { GridEdgeReactRendererProps } from './renderers/grid.js'; import { useSimpleOrChildrenRenderer } from './renderers/useSimpleOrChildrenRenderer.js'; import type { OmittedGridColumnHTMLAttributes } from './GridColumn.js'; +import useMergedRefs from './utils/useMergedRefs.js'; +import { useGridColumn } from './Grid.js'; export * from './generated/GridTreeColumn.js'; @@ -49,8 +52,14 @@ function GridTreeColumn( const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header); const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); + const innerRef = useRef>(null); + const finalRef = useMergedRefs(innerRef, ref); + + const isRendered = (!headerRenderer || headerPortals!.length > 0) && (!footerRenderer || footerPortals!.length > 0); + useGridColumn(innerRef, isRendered); + return ( - <_GridTreeColumn {...props} headerRenderer={headerRenderer} footerRenderer={footerRenderer} ref={ref}> + <_GridTreeColumn {...props} headerRenderer={headerRenderer} footerRenderer={footerRenderer} ref={finalRef}> {headerPortals} {footerPortals} diff --git a/packages/react-components/src/renderers/useRenderer.ts b/packages/react-components/src/renderers/useRenderer.ts index 77bb1949..79bb02b3 100644 --- a/packages/react-components/src/renderers/useRenderer.ts +++ b/packages/react-components/src/renderers/useRenderer.ts @@ -13,7 +13,6 @@ import type { Slice, WebComponentRenderer } from './renderer.js'; export type UseRendererResult = readonly [ portals?: ReadonlyArray, renderer?: W, - isRendered?: boolean, ]; const initialState = new Map(); @@ -80,7 +79,6 @@ export function useRenderer

( ), ), renderer, - map.size > 0 ] : []; } From 5abefe6398c4e50efbf65dbd65863150ce18e1df Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Fri, 15 Nov 2024 16:35:10 +0400 Subject: [PATCH 06/29] remove unused files --- packages/react-components/src/Grid.tsx | 2 +- .../react-components/src/utils/markElementAsRendered.ts | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) delete mode 100644 packages/react-components/src/utils/markElementAsRendered.ts diff --git a/packages/react-components/src/Grid.tsx b/packages/react-components/src/Grid.tsx index 94a02c34..59ea3583 100644 --- a/packages/react-components/src/Grid.tsx +++ b/packages/react-components/src/Grid.tsx @@ -26,7 +26,7 @@ import type { GridSelectionColumnElement } from './GridSelectionColumn.js'; import type { GridFilterColumnElement } from './GridFilterColumn.js'; import type { GridSortColumnElement } from './GridSortColumn.js'; import type { GridTreeColumnElement } from './GridTreeColumn.js'; -import { GridColumnGroupElement } from './GridColumnGroup.js'; +import type { GridColumnGroupElement } from './GridColumnGroup.js'; export * from './generated/Grid.js'; diff --git a/packages/react-components/src/utils/markElementAsRendered.ts b/packages/react-components/src/utils/markElementAsRendered.ts deleted file mode 100644 index 94846977..00000000 --- a/packages/react-components/src/utils/markElementAsRendered.ts +++ /dev/null @@ -1,9 +0,0 @@ -const ELEMENT_RENDERED = Symbol(); - -export function markElementAsRendered(element: HTMLElement & { [ELEMENT_RENDERED]?: boolean }) { - element[ELEMENT_RENDERED] = true; -} - -export function isElementMarkedAsRendered(element: HTMLElement & { [ELEMENT_RENDERED]?: boolean }) { - return !!element[ELEMENT_RENDERED]; -} From 89ca6033aeab6517dcb9469f4f9d05c1e9a03f4b Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Fri, 15 Nov 2024 18:24:02 +0400 Subject: [PATCH 07/29] use different approach --- .../src/GridProEditColumn.tsx | 8 +- packages/react-components/src/Grid.tsx | 96 ++++--------------- packages/react-components/src/GridColumn.tsx | 14 +-- .../react-components/src/GridColumnGroup.tsx | 13 +-- .../react-components/src/GridFilterColumn.tsx | 11 +-- .../src/GridSelectionColumn.tsx | 13 --- .../react-components/src/GridSortColumn.tsx | 11 +-- .../react-components/src/GridTreeColumn.tsx | 11 +-- 8 files changed, 23 insertions(+), 154 deletions(-) diff --git a/packages/react-components-pro/src/GridProEditColumn.tsx b/packages/react-components-pro/src/GridProEditColumn.tsx index 3aa20bcb..19399683 100644 --- a/packages/react-components-pro/src/GridProEditColumn.tsx +++ b/packages/react-components-pro/src/GridProEditColumn.tsx @@ -136,12 +136,8 @@ function GridProEditColumn( const [editModePortals, editModeRenderer] = useModelRenderer(editColumnReactRenderer(props.editModeRenderer), { renderSync: true, }); - const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header, { - renderSync: true, - }); - const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer, { - renderSync: true, - }); + const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header); + const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); const [bodyPortals, bodyRenderer] = useModelRenderer(editColumnReactRenderer(props.renderer ?? children), { renderSync: true, }); diff --git a/packages/react-components/src/Grid.tsx b/packages/react-components/src/Grid.tsx index 59ea3583..7f6e2728 100644 --- a/packages/react-components/src/Grid.tsx +++ b/packages/react-components/src/Grid.tsx @@ -1,13 +1,9 @@ import { type ComponentType, - createContext, type ForwardedRef, forwardRef, type ReactElement, type RefAttributes, - type RefObject, - useCallback, - useContext, useEffect, useRef, useState, @@ -21,12 +17,6 @@ import { import type { GridRowDetailsReactRendererProps } from './renderers/grid.js'; import { useModelRenderer } from './renderers/useModelRenderer.js'; import useMergedRefs from './utils/useMergedRefs.js'; -import type { GridColumnElement } from './GridColumn.js'; -import type { GridSelectionColumnElement } from './GridSelectionColumn.js'; -import type { GridFilterColumnElement } from './GridFilterColumn.js'; -import type { GridSortColumnElement } from './GridSortColumn.js'; -import type { GridTreeColumnElement } from './GridTreeColumn.js'; -import type { GridColumnGroupElement } from './GridColumnGroup.js'; export * from './generated/Grid.js'; @@ -35,91 +25,37 @@ export type GridProps = Partial, 'rowDetailsRender rowDetailsRenderer?: ComponentType> | null; }>; -type AnyGridColumnElement = - | GridColumnElement - | GridSelectionColumnElement - | GridFilterColumnElement - | GridSortColumnElement - | GridTreeColumnElement - | GridColumnGroupElement; - -type GridContext = { - onColumnAdded(column: AnyGridColumnElement): void; - onColumnRemoved(column: AnyGridColumnElement): void; - onColumnRendered(column: AnyGridColumnElement): void; -}; - -const GridContext = createContext(null); - -export function useGridColumn(columnRef: RefObject, isRendered: boolean) { - const gridContext = useContext(GridContext)!; - - useEffect(() => { - gridContext.onColumnAdded(columnRef.current!); - - return () => { - gridContext.onColumnRemoved(columnRef.current!); - }; - }, []); - - useEffect(() => { - if (isRendered) { - gridContext.onColumnRendered(columnRef.current!); - } - }, [isRendered]); -} - function Grid( props: GridProps, ref: ForwardedRef>, ): ReactElement | null { const [portals, rowDetailsRenderer] = useModelRenderer(props.rowDetailsRenderer); - const innerRef = useRef(null); + const innerRef = useRef(null); const finalRef = useMergedRefs(innerRef, ref); - const columnsRef = useRef>(new Set()); - const [renderedColumns, setRenderedColumns] = useState>(new Set()); - const [isColumnsWidthRecalculationPending, setColumnsWidthRecalculationPending] = useState(true); - - const onColumnAdded = useCallback((column: AnyGridColumnElement) => { - columnsRef.current.add(column); - }, []); - - const onColumnRemoved = useCallback((column: AnyGridColumnElement) => { - columnsRef.current.delete(column); + const [pendingRecalculateColumnWidthsCall, setPendingRecalculateColumnWidthsCall] = useState(null); - setRenderedColumns((columns) => { - columns.delete(column); - return new Set(columns); - }); - }, []); - - const onColumnRendered = useCallback((column: AnyGridColumnElement) => { - setRenderedColumns((columns) => { - columns.add(column); - return new Set(columns); - }); + useEffect(() => { + innerRef.current!._recalculateColumnWidths = function (...args) { + setPendingRecalculateColumnWidthsCall(args); + }; }, []); useEffect(() => { - if (!isColumnsWidthRecalculationPending || columnsRef.current.size === 0) { - return; - } - - if ([...columnsRef.current].every((col) => col.hidden || renderedColumns.has(col))) { - innerRef.current!.recalculateColumnWidths(); - setColumnsWidthRecalculationPending(false); + if (pendingRecalculateColumnWidthsCall) { + const gridElement = innerRef.current!; + const gridProto = Object.getPrototypeOf(gridElement); + gridProto._recalculateColumnWidths.call(gridElement, ...pendingRecalculateColumnWidthsCall); + setPendingRecalculateColumnWidthsCall(null); } - }, [renderedColumns, isColumnsWidthRecalculationPending]); + }, [pendingRecalculateColumnWidthsCall]); return ( - - <_Grid {...props} ref={finalRef} rowDetailsRenderer={rowDetailsRenderer}> - {props.children} - {portals} - - + <_Grid {...props} ref={finalRef} rowDetailsRenderer={rowDetailsRenderer}> + {props.children} + {portals} + ); } diff --git a/packages/react-components/src/GridColumn.tsx b/packages/react-components/src/GridColumn.tsx index 0b0c1584..811ba89c 100644 --- a/packages/react-components/src/GridColumn.tsx +++ b/packages/react-components/src/GridColumn.tsx @@ -5,7 +5,6 @@ import { type ReactElement, type ReactNode, type RefAttributes, - useRef, } from 'react'; import type { GridDefaultItem } from './generated/Grid.js'; import { @@ -16,8 +15,6 @@ import { import type { GridBodyReactRendererProps, GridEdgeReactRendererProps } from './renderers/grid.js'; import { useModelRenderer } from './renderers/useModelRenderer.js'; import { useSimpleOrChildrenRenderer } from './renderers/useSimpleOrChildrenRenderer.js'; -import useMergedRefs from './utils/useMergedRefs.js'; -import { useGridColumn } from './Grid.js'; export * from './generated/GridColumn.js'; @@ -61,21 +58,12 @@ function GridColumn( const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? children); - const innerRef = useRef>(null); - const finalRef = useMergedRefs(innerRef, ref); - - const isRendered = - (!headerRenderer || headerPortals!.length > 0) && - (!footerRenderer || footerPortals!.length > 0) && - (!bodyRenderer || bodyPortals!.length > 0); - useGridColumn(innerRef, isRendered); - return ( <_GridColumn {...props} footerRenderer={footerRenderer} headerRenderer={headerRenderer} - ref={finalRef} + ref={ref} renderer={bodyRenderer} > {headerPortals} diff --git a/packages/react-components/src/GridColumnGroup.tsx b/packages/react-components/src/GridColumnGroup.tsx index bb031ec2..3bd0eaa3 100644 --- a/packages/react-components/src/GridColumnGroup.tsx +++ b/packages/react-components/src/GridColumnGroup.tsx @@ -1,8 +1,5 @@ import { forwardRef, - useContext, - useEffect, - useRef, type ComponentType, type ForwardedRef, type ReactElement, @@ -17,8 +14,6 @@ import { import { type ReactSimpleRendererProps } from './renderers/useSimpleRenderer.js'; import { useSimpleOrChildrenRenderer } from './renderers/useSimpleOrChildrenRenderer.js'; import type { OmittedGridColumnHTMLAttributes } from './GridColumn.js'; -import useMergedRefs from './utils/useMergedRefs.js'; -import { useGridColumn } from './Grid.js'; export * from './generated/GridColumnGroup.js'; @@ -48,14 +43,8 @@ function GridColumnGroup( const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header); const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); - const innerRef = useRef(null); - const finalRef = useMergedRefs(innerRef, ref); - - const isRendered = (!headerRenderer || headerPortals!.length > 0) && (!footerRenderer || footerPortals!.length > 0); - useGridColumn(innerRef, isRendered); - return ( - <_GridColumnGroup {...props} footerRenderer={footerRenderer} headerRenderer={headerRenderer} ref={finalRef}> + <_GridColumnGroup {...props} footerRenderer={footerRenderer} headerRenderer={headerRenderer} ref={ref}> {headerPortals} {footerPortals} {children} diff --git a/packages/react-components/src/GridFilterColumn.tsx b/packages/react-components/src/GridFilterColumn.tsx index 3bd1d7f1..741c3366 100644 --- a/packages/react-components/src/GridFilterColumn.tsx +++ b/packages/react-components/src/GridFilterColumn.tsx @@ -5,7 +5,6 @@ import { type ReactElement, type ReactNode, type RefAttributes, - useRef, } from 'react'; import type { GridDefaultItem } from './generated/Grid.js'; import { @@ -17,8 +16,6 @@ import type { GridBodyReactRendererProps, GridEdgeReactRendererProps } from './r import { useModelRenderer } from './renderers/useModelRenderer.js'; import { useSimpleOrChildrenRenderer } from './renderers/useSimpleOrChildrenRenderer.js'; import type { OmittedGridColumnHTMLAttributes } from './GridColumn.js'; -import useMergedRefs from './utils/useMergedRefs.js'; -import { useGridColumn } from './Grid.js'; export * from './generated/GridFilterColumn.js'; @@ -49,14 +46,8 @@ function GridFilterColumn( const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? props.children); - const innerRef = useRef>(null); - const finalRef = useMergedRefs(innerRef, ref); - - const isRendered = (!footerRenderer || footerPortals!.length > 0) && (!bodyRenderer || bodyPortals!.length > 0); - useGridColumn(innerRef, isRendered); - return ( - <_GridFilterColumn {...props} footerRenderer={footerRenderer} ref={finalRef} renderer={bodyRenderer}> + <_GridFilterColumn {...props} footerRenderer={footerRenderer} ref={ref} renderer={bodyRenderer}> {footerPortals} {bodyPortals} diff --git a/packages/react-components/src/GridSelectionColumn.tsx b/packages/react-components/src/GridSelectionColumn.tsx index c07573d7..32d2a343 100644 --- a/packages/react-components/src/GridSelectionColumn.tsx +++ b/packages/react-components/src/GridSelectionColumn.tsx @@ -5,7 +5,6 @@ import { type ReactElement, type ReactNode, type RefAttributes, - useRef, } from 'react'; import type { GridDefaultItem } from './generated/Grid.js'; import { @@ -17,8 +16,6 @@ import type { GridBodyReactRendererProps, GridEdgeReactRendererProps } from './r import { useModelRenderer } from './renderers/useModelRenderer.js'; import { useSimpleOrChildrenRenderer } from './renderers/useSimpleOrChildrenRenderer.js'; import type { OmittedGridColumnHTMLAttributes } from './GridColumn.js'; -import { useGridColumn } from './Grid.js'; -import useMergedRefs from './utils/useMergedRefs.js'; export * from './generated/GridSelectionColumn.js'; @@ -56,21 +53,11 @@ function GridSelectionColumn( const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? props.children); - const innerRef = useRef>(null); - const finalRef = useMergedRefs(innerRef, ref); - - const isRendered = - (!headerRenderer || headerPortals!.length > 0) && - (!footerRenderer || footerPortals!.length > 0) && - (!bodyRenderer || bodyPortals!.length > 0); - useGridColumn(innerRef, isRendered); - return ( <_GridSelectionColumn {...props} footerRenderer={footerRenderer} headerRenderer={headerRenderer} - ref={finalRef} renderer={bodyRenderer} > {headerPortals} diff --git a/packages/react-components/src/GridSortColumn.tsx b/packages/react-components/src/GridSortColumn.tsx index e29fb542..d460c527 100644 --- a/packages/react-components/src/GridSortColumn.tsx +++ b/packages/react-components/src/GridSortColumn.tsx @@ -5,7 +5,6 @@ import { type ReactElement, type ReactNode, type RefAttributes, - useRef, } from 'react'; import type { GridDefaultItem } from './generated/Grid.js'; import { @@ -17,8 +16,6 @@ import type { GridBodyReactRendererProps, GridEdgeReactRendererProps } from './r import { useModelRenderer } from './renderers/useModelRenderer.js'; import { useSimpleOrChildrenRenderer } from './renderers/useSimpleOrChildrenRenderer.js'; import type { OmittedGridColumnHTMLAttributes } from './GridColumn.js'; -import useMergedRefs from './utils/useMergedRefs.js'; -import { useGridColumn } from './Grid.js'; export * from './generated/GridSortColumn.js'; @@ -48,14 +45,8 @@ function GridSortColumn( const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? props.children); - const innerRef = useRef>(null); - const finalRef = useMergedRefs(innerRef, ref); - - const isRendered = (!footerRenderer || footerPortals!.length > 0) && (!bodyRenderer || bodyPortals!.length > 0); - useGridColumn(innerRef, isRendered); - return ( - <_GridSortColumn {...props} footerRenderer={footerRenderer} ref={finalRef} renderer={bodyRenderer}> + <_GridSortColumn {...props} footerRenderer={footerRenderer} ref={ref} renderer={bodyRenderer}> {footerPortals} {bodyPortals} diff --git a/packages/react-components/src/GridTreeColumn.tsx b/packages/react-components/src/GridTreeColumn.tsx index 95d0b84b..fcd9cedf 100644 --- a/packages/react-components/src/GridTreeColumn.tsx +++ b/packages/react-components/src/GridTreeColumn.tsx @@ -5,7 +5,6 @@ import { type ReactElement, type ReactNode, type RefAttributes, - useRef, } from 'react'; import type { GridDefaultItem } from './generated/Grid.js'; import { @@ -16,8 +15,6 @@ import { import type { GridEdgeReactRendererProps } from './renderers/grid.js'; import { useSimpleOrChildrenRenderer } from './renderers/useSimpleOrChildrenRenderer.js'; import type { OmittedGridColumnHTMLAttributes } from './GridColumn.js'; -import useMergedRefs from './utils/useMergedRefs.js'; -import { useGridColumn } from './Grid.js'; export * from './generated/GridTreeColumn.js'; @@ -52,14 +49,8 @@ function GridTreeColumn( const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header); const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); - const innerRef = useRef>(null); - const finalRef = useMergedRefs(innerRef, ref); - - const isRendered = (!headerRenderer || headerPortals!.length > 0) && (!footerRenderer || footerPortals!.length > 0); - useGridColumn(innerRef, isRendered); - return ( - <_GridTreeColumn {...props} headerRenderer={headerRenderer} footerRenderer={footerRenderer} ref={finalRef}> + <_GridTreeColumn {...props} headerRenderer={headerRenderer} footerRenderer={footerRenderer} ref={ref}> {headerPortals} {footerPortals} From 2f6f8d960dfc0e40579c5ce87d82f65c1906cff5 Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Fri, 15 Nov 2024 18:24:24 +0400 Subject: [PATCH 08/29] update tests to respect async rendering --- .../src/GridSelectionColumn.tsx | 1 + test/Grid.spec.tsx | 137 ++++++++++++------ 2 files changed, 97 insertions(+), 41 deletions(-) diff --git a/packages/react-components/src/GridSelectionColumn.tsx b/packages/react-components/src/GridSelectionColumn.tsx index 32d2a343..223b6dcf 100644 --- a/packages/react-components/src/GridSelectionColumn.tsx +++ b/packages/react-components/src/GridSelectionColumn.tsx @@ -58,6 +58,7 @@ function GridSelectionColumn( {...props} footerRenderer={footerRenderer} headerRenderer={headerRenderer} + ref={ref} renderer={bodyRenderer} > {headerPortals} diff --git a/test/Grid.spec.tsx b/test/Grid.spec.tsx index e73a6d90..8424dee1 100644 --- a/test/Grid.spec.tsx +++ b/test/Grid.spec.tsx @@ -1,6 +1,6 @@ import { expect, use as useChaiPlugin } from '@esm-bundle/chai'; import chaiDom from 'chai-dom'; -import { cleanup, render } from '@testing-library/react/pure.js'; +import { cleanup, render, waitFor } from '@testing-library/react/pure.js'; import { Grid, type GridDataProvider } from '../packages/react-components/src/Grid.js'; import { GridColumn, GridColumnElement } from '../packages/react-components/src/GridColumn.js'; import { GridFilterColumn } from '../packages/react-components/src/GridFilterColumn.js'; @@ -9,7 +9,6 @@ import { GridSelectionColumn } from '../packages/react-components/src/GridSelect import { GridSortColumn } from '../packages/react-components/src/GridSortColumn.js'; import { GridTreeColumn } from '../packages/react-components/src/GridTreeColumn.js'; import type { GridBodyReactRendererProps } from '../packages/react-components/src/renderers/grid.js'; -import catchRender from './utils/catchRender.js'; import { GridColumnGroup } from '../packages/react-components/src/GridColumnGroup.js'; import { findByQuerySelector } from './utils/findByQuerySelector.js'; import { GridPro } from '../packages/react-components-pro/src/GridPro.js'; @@ -63,12 +62,10 @@ describe('Grid', () => { ); } - async function getGridMeaningfulParts(columnElementName: string) { + function getGridMeaningfulParts(columnElementName: string) { const grid = document.querySelector('vaadin-grid, vaadin-grid-pro')!; expect(grid).to.exist; - await catchRender(grid, isGridCellContentNodeRendered); - const columns = document.querySelectorAll(columnElementName); // Filter cells that don't have any textContent. Grid creates empty cells for some calculations, @@ -102,10 +99,14 @@ describe('Grid', () => { , ); - const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-column'); + const [_columns, cells] = await waitFor(async () => { + const [columns, cells] = getGridMeaningfulParts('vaadin-grid-column'); + + expect(columns).to.have.length(3); + expect(cells).to.have.length(15); - expect(columns).to.have.length(3); - expect(cells).to.have.length(15); + return [columns, cells]; + }); const [headerRendererCell, headerInlineCell, nameHeaderCell, surnameHeaderCell, roleHeaderCell] = cells.slice( 0, @@ -217,10 +218,14 @@ describe('Grid', () => { , ); - const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-column'); + const [_columns, cells] = await waitFor(async () => { + const [columns, cells] = getGridMeaningfulParts('vaadin-grid-column'); - expect(columns).to.have.length(1); - expect(cells).to.have.length(6); + expect(columns).to.have.length(1); + expect(cells).to.have.length(6); + + return [columns, cells]; + }); const [groupHeaderCell, nameHeaderCell, nameFooterCell, groupFooterCell] = cells; @@ -239,9 +244,14 @@ describe('Grid', () => { , ); - const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-filter-column'); - expect(columns).to.have.length(1); - expect(cells).to.have.length(3); + const [_columns, cells] = await waitFor(async () => { + const [columns, cells] = getGridMeaningfulParts('vaadin-grid-filter-column'); + + expect(columns).to.have.length(1); + expect(cells).to.have.length(3); + + return [columns, cells]; + }); const [footerCell, bodyCell1, bodyCell2] = cells; @@ -257,9 +267,14 @@ describe('Grid', () => { , ); - const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-filter-column'); - expect(columns).to.have.length(1); - expect(cells).to.have.length(4); + const [_columns, cells] = await waitFor(async () => { + const [columns, cells] = getGridMeaningfulParts('vaadin-grid-filter-column'); + + expect(columns).to.have.length(1); + expect(cells).to.have.length(4); + + return [columns, cells]; + }); const footerCell = cells[1]; @@ -277,9 +292,14 @@ describe('Grid', () => { , ); - const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-selection-column'); - expect(columns).to.have.length(1); - expect(cells).to.have.length(4); + const [_columns, cells] = await waitFor(async () => { + const [columns, cells] = getGridMeaningfulParts('vaadin-grid-selection-column'); + + expect(columns).to.have.length(1); + expect(cells).to.have.length(4); + + return [columns, cells]; + }); const [headerCell, footerCell, bodyCell1, bodyCell2] = cells; @@ -298,9 +318,14 @@ describe('Grid', () => { , ); - const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-selection-column'); - expect(columns).to.have.length(1); - expect(cells).to.have.length(4); + const [_columns, cells] = await waitFor(async () => { + const [columns, cells] = getGridMeaningfulParts('vaadin-grid-selection-column'); + + expect(columns).to.have.length(1); + expect(cells).to.have.length(4); + + return [columns, cells]; + }); const [headerCell, footerCell] = cells; @@ -317,9 +342,14 @@ describe('Grid', () => { , ); - const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-sort-column'); - expect(columns).to.have.length(1); - expect(cells).to.have.length(3); + const [_columns, cells] = await waitFor(async () => { + const [columns, cells] = getGridMeaningfulParts('vaadin-grid-sort-column'); + + expect(columns).to.have.length(1); + expect(cells).to.have.length(3); + + return [columns, cells]; + }); const [footerCell, bodyCell1, bodyCell2] = cells; @@ -335,9 +365,14 @@ describe('Grid', () => { , ); - const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-sort-column'); - expect(columns).to.have.length(1); - expect(cells).to.have.length(4); + const [_columns, cells] = await waitFor(async () => { + const [columns, cells] = getGridMeaningfulParts('vaadin-grid-sort-column'); + + expect(columns).to.have.length(1); + expect(cells).to.have.length(4); + + return [columns, cells]; + }); const footerCell = cells[1]; @@ -355,9 +390,14 @@ describe('Grid', () => { , ); - const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-pro-edit-column'); - expect(columns).to.have.length(1); - expect(cells).to.have.length(4); + const [_columns, cells] = await waitFor(async () => { + const [columns, cells] = getGridMeaningfulParts('vaadin-grid-pro-edit-column'); + + expect(columns).to.have.length(1); + expect(cells).to.have.length(4); + + return [columns, cells]; + }); const [headerCell, footerCell, bodyCell1, bodyCell2] = cells; @@ -374,9 +414,14 @@ describe('Grid', () => { , ); - const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-pro-edit-column'); - expect(columns).to.have.length(1); - expect(cells).to.have.length(4); + const [_columns, cells] = await waitFor(async () => { + const [columns, cells] = getGridMeaningfulParts('vaadin-grid-pro-edit-column'); + + expect(columns).to.have.length(1); + expect(cells).to.have.length(4); + + return [columns, cells]; + }); const [headerCell, footerCell] = cells; @@ -585,9 +630,14 @@ describe('Grid', () => { , ); - const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-tree-column'); - expect(columns).to.have.length(1); - expect(cells).to.have.length(7); + const [_columns, cells] = await waitFor(async () => { + const [columns, cells] = getGridMeaningfulParts('vaadin-grid-tree-column'); + + expect(columns).to.have.length(1); + expect(cells).to.have.length(7); + + return [columns, cells]; + }); const [treeHeaderCell, nameHeaderCell, treeFooterCell] = cells; @@ -604,9 +654,14 @@ describe('Grid', () => { , ); - const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-tree-column'); - expect(columns).to.have.length(1); - expect(cells).to.have.length(7); + const [_columns, cells] = await waitFor(async () => { + const [columns, cells] = getGridMeaningfulParts('vaadin-grid-tree-column'); + + expect(columns).to.have.length(1); + expect(cells).to.have.length(7); + + return [columns, cells]; + }); const [treeHeaderCell, nameHeaderCell, treeFooterCell] = cells; From 7cac0736e49eede00b889fd0ad807184ea71eddd Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Mon, 18 Nov 2024 16:56:10 +0400 Subject: [PATCH 09/29] fix: override grid method in useLayoutEffect --- packages/react-components/src/Grid.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-components/src/Grid.tsx b/packages/react-components/src/Grid.tsx index 7f6e2728..d7d9be2b 100644 --- a/packages/react-components/src/Grid.tsx +++ b/packages/react-components/src/Grid.tsx @@ -5,6 +5,7 @@ import { type ReactElement, type RefAttributes, useEffect, + useLayoutEffect, useRef, useState, } from 'react'; @@ -36,7 +37,7 @@ function Grid( const [pendingRecalculateColumnWidthsCall, setPendingRecalculateColumnWidthsCall] = useState(null); - useEffect(() => { + useLayoutEffect(() => { innerRef.current!._recalculateColumnWidths = function (...args) { setPendingRecalculateColumnWidthsCall(args); }; From 88dc239de01457094b7d1def7fbfa755db4e34d5 Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Tue, 19 Nov 2024 16:13:12 +0400 Subject: [PATCH 10/29] refactor: recalculate column widths also in layout effect --- packages/react-components/src/Grid.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/src/Grid.tsx b/packages/react-components/src/Grid.tsx index d7d9be2b..8e63e9ca 100644 --- a/packages/react-components/src/Grid.tsx +++ b/packages/react-components/src/Grid.tsx @@ -43,7 +43,7 @@ function Grid( }; }, []); - useEffect(() => { + useLayoutEffect(() => { if (pendingRecalculateColumnWidthsCall) { const gridElement = innerRef.current!; const gridProto = Object.getPrototypeOf(gridElement); From f735420951a6a4bd03a8e24b593d99e9fcc6ef3f Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Tue, 19 Nov 2024 18:31:47 +0400 Subject: [PATCH 11/29] use async rendering but defer it to microtask --- .../src/GridProEditColumn.tsx | 12 ++++--- packages/react-components/src/Grid.tsx | 25 +++++-------- packages/react-components/src/GridColumn.tsx | 12 +++++-- .../react-components/src/GridColumnGroup.tsx | 8 +++-- .../react-components/src/GridFilterColumn.tsx | 8 +++-- .../src/GridSelectionColumn.tsx | 12 +++++-- .../react-components/src/GridSortColumn.tsx | 8 +++-- .../react-components/src/GridTreeColumn.tsx | 8 +++-- .../src/renderers/useRenderer.ts | 35 +++++++++++-------- 9 files changed, 78 insertions(+), 50 deletions(-) diff --git a/packages/react-components-pro/src/GridProEditColumn.tsx b/packages/react-components-pro/src/GridProEditColumn.tsx index 19399683..c265a9df 100644 --- a/packages/react-components-pro/src/GridProEditColumn.tsx +++ b/packages/react-components-pro/src/GridProEditColumn.tsx @@ -134,12 +134,16 @@ function GridProEditColumn( ref: ForwardedRef>, ): ReactElement | null { const [editModePortals, editModeRenderer] = useModelRenderer(editColumnReactRenderer(props.editModeRenderer), { - renderSync: true, + renderMode: 'sync', + }); + const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header, { + renderMode: 'microtask', + }); + const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer, { + renderMode: 'microtask', }); - const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header); - const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); const [bodyPortals, bodyRenderer] = useModelRenderer(editColumnReactRenderer(props.renderer ?? children), { - renderSync: true, + renderMode: 'sync', }); return ( diff --git a/packages/react-components/src/Grid.tsx b/packages/react-components/src/Grid.tsx index 8e63e9ca..6de33145 100644 --- a/packages/react-components/src/Grid.tsx +++ b/packages/react-components/src/Grid.tsx @@ -4,10 +4,8 @@ import { forwardRef, type ReactElement, type RefAttributes, - useEffect, useLayoutEffect, useRef, - useState, } from 'react'; import { Grid as _Grid, @@ -30,28 +28,21 @@ function Grid( props: GridProps, ref: ForwardedRef>, ): ReactElement | null { - const [portals, rowDetailsRenderer] = useModelRenderer(props.rowDetailsRenderer); + const [portals, rowDetailsRenderer] = useModelRenderer(props.rowDetailsRenderer, { + renderMode: 'microtask' + }); - const innerRef = useRef(null); + const innerRef = useRef(null); const finalRef = useMergedRefs(innerRef, ref); - const [pendingRecalculateColumnWidthsCall, setPendingRecalculateColumnWidthsCall] = useState(null); - useLayoutEffect(() => { - innerRef.current!._recalculateColumnWidths = function (...args) { - setPendingRecalculateColumnWidthsCall(args); + innerRef.current!.recalculateColumnWidths = function (...args) { + queueMicrotask(() => { + Object.getPrototypeOf(this).recalculateColumnWidths.call(this, ...args); + }); }; }, []); - useLayoutEffect(() => { - if (pendingRecalculateColumnWidthsCall) { - const gridElement = innerRef.current!; - const gridProto = Object.getPrototypeOf(gridElement); - gridProto._recalculateColumnWidths.call(gridElement, ...pendingRecalculateColumnWidthsCall); - setPendingRecalculateColumnWidthsCall(null); - } - }, [pendingRecalculateColumnWidthsCall]); - return ( <_Grid {...props} ref={finalRef} rowDetailsRenderer={rowDetailsRenderer}> {props.children} diff --git a/packages/react-components/src/GridColumn.tsx b/packages/react-components/src/GridColumn.tsx index 811ba89c..2b933518 100644 --- a/packages/react-components/src/GridColumn.tsx +++ b/packages/react-components/src/GridColumn.tsx @@ -54,9 +54,15 @@ function GridColumn( { children, footer, header, ...props }: GridColumnProps, ref: ForwardedRef>, ): ReactElement | null { - const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header); - const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); - const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? children); + const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header, { + renderMode: 'microtask', + }); + const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer, { + renderMode: 'microtask', + }); + const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? children, { + renderMode: 'microtask', + }); return ( <_GridColumn diff --git a/packages/react-components/src/GridColumnGroup.tsx b/packages/react-components/src/GridColumnGroup.tsx index 3bd0eaa3..9a89f422 100644 --- a/packages/react-components/src/GridColumnGroup.tsx +++ b/packages/react-components/src/GridColumnGroup.tsx @@ -40,8 +40,12 @@ function GridColumnGroup( { children, footer, header, ...props }: GridColumnGroupProps, ref: ForwardedRef, ): ReactElement | null { - const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header); - const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); + const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header, { + renderMode: 'microtask', + }); + const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer, { + renderMode: 'microtask', + }); return ( <_GridColumnGroup {...props} footerRenderer={footerRenderer} headerRenderer={headerRenderer} ref={ref}> diff --git a/packages/react-components/src/GridFilterColumn.tsx b/packages/react-components/src/GridFilterColumn.tsx index 741c3366..4a26ccbb 100644 --- a/packages/react-components/src/GridFilterColumn.tsx +++ b/packages/react-components/src/GridFilterColumn.tsx @@ -43,8 +43,12 @@ function GridFilterColumn( { footer, ...props }: GridFilterColumnProps, ref: ForwardedRef>, ): ReactElement | null { - const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); - const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? props.children); + const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer, { + renderMode: 'microtask', + }); + const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? props.children, { + renderMode: 'microtask', + }); return ( <_GridFilterColumn {...props} footerRenderer={footerRenderer} ref={ref} renderer={bodyRenderer}> diff --git a/packages/react-components/src/GridSelectionColumn.tsx b/packages/react-components/src/GridSelectionColumn.tsx index 223b6dcf..b0f23d5e 100644 --- a/packages/react-components/src/GridSelectionColumn.tsx +++ b/packages/react-components/src/GridSelectionColumn.tsx @@ -49,9 +49,15 @@ function GridSelectionColumn( { footer, header, ...props }: GridSelectionColumnProps, ref: ForwardedRef>, ): ReactElement | null { - const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header); - const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); - const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? props.children); + const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header, { + renderMode: 'microtask', + }); + const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer, { + renderMode: 'microtask', + }); + const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? props.children, { + renderMode: 'microtask', + }); return ( <_GridSelectionColumn diff --git a/packages/react-components/src/GridSortColumn.tsx b/packages/react-components/src/GridSortColumn.tsx index d460c527..76c2bfb7 100644 --- a/packages/react-components/src/GridSortColumn.tsx +++ b/packages/react-components/src/GridSortColumn.tsx @@ -42,8 +42,12 @@ function GridSortColumn( { footer, ...props }: GridSortColumnProps, ref: ForwardedRef>, ): ReactElement | null { - const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); - const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? props.children); + const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer, { + renderMode: 'microtask', + }); + const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? props.children, { + renderMode: 'microtask', + }); return ( <_GridSortColumn {...props} footerRenderer={footerRenderer} ref={ref} renderer={bodyRenderer}> diff --git a/packages/react-components/src/GridTreeColumn.tsx b/packages/react-components/src/GridTreeColumn.tsx index fcd9cedf..2712b622 100644 --- a/packages/react-components/src/GridTreeColumn.tsx +++ b/packages/react-components/src/GridTreeColumn.tsx @@ -46,8 +46,12 @@ function GridTreeColumn( { footer, header, ...props }: GridTreeColumnProps, ref: ForwardedRef>, ): ReactElement | null { - const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header); - const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer); + const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header, { + renderMode: 'microtask', + }); + const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer, { + renderMode: 'microtask', + }); return ( <_GridTreeColumn {...props} headerRenderer={headerRenderer} footerRenderer={footerRenderer} ref={ref}> diff --git a/packages/react-components/src/renderers/useRenderer.ts b/packages/react-components/src/renderers/useRenderer.ts index 79bb02b3..54404002 100644 --- a/packages/react-components/src/renderers/useRenderer.ts +++ b/packages/react-components/src/renderers/useRenderer.ts @@ -25,9 +25,25 @@ function rendererReducer( } export type RendererConfig = { - renderSync?: boolean; + renderMode?: 'default' | 'sync' | 'microtask'; }; +const renderQueue: Array<(...args: any[]) => any> = []; + +function flushMicrotask(callback: (...args: any[]) => any) { + renderQueue.push(callback); + + if (renderQueue.length === 1) { + queueMicrotask(() => { + flushSync(() => { + while (renderQueue.length) { + renderQueue.shift()!(); + } + }); + }); + } +} + export function useRenderer

( node: ReactNode, convert?: (props: Slice, 1>) => PropsWithChildren

, @@ -46,21 +62,10 @@ export function useRenderer

( const [map, update] = useReducer>(rendererReducer, initialState); const renderer = useCallback( ((...args: Parameters) => { - if (config?.renderSync) { - // The web components may request multiple synchronous renderer calls that - // would result in flushSync logging a warning (and actually executing the - // overlapping flushSync in microtask timing). Suppress the warning and allow - // the resulting asynchronicity. - const console = globalThis.console as any; - const error = console.error; - console.error = (message: string) => { - if (message.includes('flushSync')) { - return; - } - error(message); - }; + if (config?.renderMode === 'microtask') { + flushMicrotask(() => update(args)); + } else if (config?.renderMode === 'sync') { flushSync(() => update(args)); - console.error = error; } else { update(args); } From e61ea860c31a7dcba6c57a0d5112493391042918 Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Wed, 20 Nov 2024 11:26:50 +0400 Subject: [PATCH 12/29] extract flushMicrotask as a helper, improve implementation --- .../src/renderers/useRenderer.ts | 17 +---------------- .../src/utils/flushMicrotask.ts | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 16 deletions(-) create mode 100644 packages/react-components/src/utils/flushMicrotask.ts diff --git a/packages/react-components/src/renderers/useRenderer.ts b/packages/react-components/src/renderers/useRenderer.ts index 54404002..3f961089 100644 --- a/packages/react-components/src/renderers/useRenderer.ts +++ b/packages/react-components/src/renderers/useRenderer.ts @@ -9,6 +9,7 @@ import { } from 'react'; import { createPortal, flushSync } from 'react-dom'; import type { Slice, WebComponentRenderer } from './renderer.js'; +import { flushMicrotask } from '../utils/flushMicrotask.js'; export type UseRendererResult = readonly [ portals?: ReadonlyArray, @@ -28,22 +29,6 @@ export type RendererConfig = { renderMode?: 'default' | 'sync' | 'microtask'; }; -const renderQueue: Array<(...args: any[]) => any> = []; - -function flushMicrotask(callback: (...args: any[]) => any) { - renderQueue.push(callback); - - if (renderQueue.length === 1) { - queueMicrotask(() => { - flushSync(() => { - while (renderQueue.length) { - renderQueue.shift()!(); - } - }); - }); - } -} - export function useRenderer

( node: ReactNode, convert?: (props: Slice, 1>) => PropsWithChildren

, diff --git a/packages/react-components/src/utils/flushMicrotask.ts b/packages/react-components/src/utils/flushMicrotask.ts new file mode 100644 index 00000000..3e922e7a --- /dev/null +++ b/packages/react-components/src/utils/flushMicrotask.ts @@ -0,0 +1,18 @@ +import { flushSync } from 'react-dom'; + +let callbackQueue: Function[] = []; + +export function flushMicrotask(callback: Function) { + callbackQueue.push(callback); + + if (callbackQueue.length === 1) { + queueMicrotask(() => { + const queue = callbackQueue; + callbackQueue = []; + + flushSync(() => { + queue.forEach((callback) => callback()); + }); + }); + } +} From fd06a0d1ed7afdee0ac4230757daddc838ef460b Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Wed, 20 Nov 2024 16:44:23 +0400 Subject: [PATCH 13/29] fix grid pro --- .../src/GridProEditColumn.tsx | 146 ++++++++---------- packages/react-components/package.json | 4 + .../src/renderers/useModelRenderer.ts | 2 +- .../src/renderers/useRenderer.ts | 27 ++-- .../renderers/useSimpleOrChildrenRenderer.ts | 2 +- .../src/renderers/useSimpleRenderer.ts | 2 +- 6 files changed, 88 insertions(+), 95 deletions(-) diff --git a/packages/react-components-pro/src/GridProEditColumn.tsx b/packages/react-components-pro/src/GridProEditColumn.tsx index c265a9df..60c403d7 100644 --- a/packages/react-components-pro/src/GridProEditColumn.tsx +++ b/packages/react-components-pro/src/GridProEditColumn.tsx @@ -8,17 +8,10 @@ * See https://vaadin.com/commercial-license-and-service-terms for the full * license. */ -import React from 'react'; -import { - type ForwardedRef, - forwardRef, - type ReactElement, - type ReactNode, - type RefAttributes, - createElement, -} from 'react'; +import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { type ForwardedRef, forwardRef, type ReactElement, type ReactNode, type RefAttributes } from 'react'; import type { GridBodyRenderer, GridDefaultItem } from '@vaadin/react-components/Grid.js'; -import type { GridColumnElement, GridColumnProps } from '@vaadin/react-components/GridColumn.js'; +import type { GridColumnProps } from '@vaadin/react-components/GridColumn.js'; import { GridProEditColumn as _GridProEditColumn, type GridProEditColumnElement, @@ -27,6 +20,8 @@ import { import { useModelRenderer } from '@vaadin/react-components/renderers/useModelRenderer.js'; import { useSimpleOrChildrenRenderer } from '@vaadin/react-components/renderers/useSimpleOrChildrenRenderer.js'; import type { OmittedGridColumnHTMLAttributes } from '@vaadin/react-components/GridColumn.js'; +import useMergedRefs from '../../react-components/src/utils/useMergedRefs.js'; +import { flushSync } from 'react-dom'; export * from './generated/GridProEditColumn.js'; @@ -61,80 +56,23 @@ export type GridProEditColumnProps = Partial< renderer?: GridColumnRenderer; }>; -type ReactBodyRenderer = GridColumnRenderer & { - __wrapperRenderer?: ReactBodyRenderer; +type GridProEditColumnElementInternals = { + _renderEditor(cell: HTMLElement & { [SKIP_CLEARING_CELL_CONTENT]?: boolean }, model: { item: TItem }): void; + _removeEditor(cell: HTMLElement & { [SKIP_CLEARING_CELL_CONTENT]?: boolean }, model: { item: TItem }): void; + _clearCellContent(cell: HTMLElement & { [SKIP_CLEARING_CELL_CONTENT]?: boolean }): void; }; -type EditColumnRendererRoot = HTMLElement & { __editColumnRenderer?: GridBodyRenderer }; - -type ClearFunction = (arg0: HTMLElement & { _content: EditColumnRendererRoot }) => void; - -/** - * Wraps a React renderer function to render empty when requested - * - * @returns - */ -function editColumnReactRenderer(reactBodyRenderer?: ReactBodyRenderer | null) { - if (!reactBodyRenderer) { - return undefined; - } - - reactBodyRenderer.__wrapperRenderer ||= function GridProEditColumnRenderer(props) { - // If the model has __renderEmpty set, return null, otherwise call the original renderer - return '__renderEmpty' in props.model ? null : createElement(reactBodyRenderer, props); - }; - - return reactBodyRenderer.__wrapperRenderer; -} - -/** - * Wraps a Grid body renderer function to make it request empty render before - * the GridPro edit column clears cell content. - */ -function editColumnRenderer(bodyRenderer?: (GridBodyRenderer & { __wrapperRenderer?: GridBodyRenderer }) | null) { - if (!bodyRenderer) { - return undefined; - } - - bodyRenderer.__wrapperRenderer ||= ( - root: EditColumnRendererRoot, - column: GridColumnElement & { - __originalClearCellContent?: ClearFunction; - _clearCellContent?: ClearFunction; - }, - model, - ) => { - // Patch the column's _clearCellContent function which is called internally by grid-pro - // when switching from edit mode to view mode and vice versa - if (!column.__originalClearCellContent) { - column.__originalClearCellContent = column._clearCellContent; - - column._clearCellContent = (cell) => { - const cellRoot = cell._content; - // Call the original renderer with __renderEmpty set to true to clear the content it manages - cellRoot.__editColumnRenderer?.(cellRoot, column, Object.assign({}, model, { __renderEmpty: true })); - // Call the original clearCellContent function to manually clear the cell content - column.__originalClearCellContent?.(cell); - }; - } - - // Update the cell content's renderer reference so that the correct one is used - // to render empty when the cell is cleared - root.__editColumnRenderer = bodyRenderer; - - // Call the original renderer - bodyRenderer(root, column, model); - }; - - return bodyRenderer.__wrapperRenderer; -} +const SKIP_CLEARING_CELL_CONTENT = Symbol(); function GridProEditColumn( { children, footer, header, ...props }: GridProEditColumnProps, ref: ForwardedRef>, ): ReactElement | null { - const [editModePortals, editModeRenderer] = useModelRenderer(editColumnReactRenderer(props.editModeRenderer), { + const [editedItem, setEditedItem] = useState(null); + + const [editModePortals, editModeRenderer] = useModelRenderer(props.editModeRenderer, { renderMode: 'sync', + shouldRenderPortal: (_root, _column, model) => editedItem === model.item, }); const [headerPortals, headerRenderer] = useSimpleOrChildrenRenderer(props.headerRenderer, header, { renderMode: 'microtask', @@ -142,18 +80,64 @@ function GridProEditColumn( const [footerPortals, footerRenderer] = useSimpleOrChildrenRenderer(props.footerRenderer, footer, { renderMode: 'microtask', }); - const [bodyPortals, bodyRenderer] = useModelRenderer(editColumnReactRenderer(props.renderer ?? children), { - renderMode: 'sync', + const [bodyPortals, bodyRenderer] = useModelRenderer(props.renderer ?? children, { + renderMode: 'microtask', + shouldRenderPortal: (_root, _column, model) => editedItem !== model.item, }); + const innerRef = useRef & GridProEditColumnElementInternals>(null); + const finalRef = useMergedRefs(innerRef, ref); + + useLayoutEffect(() => { + innerRef.current!._clearCellContent = function (cell) { + if (!cell[SKIP_CLEARING_CELL_CONTENT]) { + Object.getPrototypeOf(this)._clearCellContent.call(this, cell); + } + }; + }, []); + + useLayoutEffect(() => { + innerRef.current!._renderEditor = function (cell, model) { + flushSync(() => { + setEditedItem(model.item); + }); + + if (!bodyRenderer) { + this._clearCellContent(cell); + } + + cell[SKIP_CLEARING_CELL_CONTENT] = true; + Object.getPrototypeOf(this)._renderEditor.call(this, cell, model); + cell[SKIP_CLEARING_CELL_CONTENT] = false; + }; + }, [bodyRenderer]); + + useLayoutEffect(() => { + innerRef.current!._removeEditor = function (cell, model) { + if (!editModeRenderer) { + this._clearCellContent(cell); + } + + flushSync(() => { + setEditedItem((editedItem) => { + return editedItem === model.item ? null : editedItem; + }); + }); + + cell[SKIP_CLEARING_CELL_CONTENT] = true; + Object.getPrototypeOf(this)._removeEditor.call(this, cell, model); + cell[SKIP_CLEARING_CELL_CONTENT] = false; + }; + }, [editModeRenderer]); + return ( <_GridProEditColumn {...props} - editModeRenderer={editColumnRenderer(editModeRenderer)} + editModeRenderer={editModeRenderer} footerRenderer={footerRenderer} headerRenderer={headerRenderer} - ref={ref} - renderer={editColumnRenderer(bodyRenderer)} + ref={finalRef} + renderer={bodyRenderer} > {editModePortals} {headerPortals} diff --git a/packages/react-components/package.json b/packages/react-components/package.json index 1740c3c4..a1554d09 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -501,6 +501,10 @@ "./utils/createComponentWithOrderedProps.d.ts.map": "./utils/createComponentWithOrderedProps.d.ts.map", "./utils/createComponentWithOrderedProps.js": "./utils/createComponentWithOrderedProps.js", "./utils/createComponentWithOrderedProps.js.map": "./utils/createComponentWithOrderedProps.js.map", + "./utils/flushMicrotask.d.ts": "./utils/flushMicrotask.d.ts", + "./utils/flushMicrotask.d.ts.map": "./utils/flushMicrotask.d.ts.map", + "./utils/flushMicrotask.js": "./utils/flushMicrotask.js", + "./utils/flushMicrotask.js.map": "./utils/flushMicrotask.js.map", "./utils/mapItemsWithComponents.d.ts": "./utils/mapItemsWithComponents.d.ts", "./utils/mapItemsWithComponents.d.ts.map": "./utils/mapItemsWithComponents.d.ts.map", "./utils/mapItemsWithComponents.js": "./utils/mapItemsWithComponents.js", diff --git a/packages/react-components/src/renderers/useModelRenderer.ts b/packages/react-components/src/renderers/useModelRenderer.ts index 27476bd3..73728bfb 100644 --- a/packages/react-components/src/renderers/useModelRenderer.ts +++ b/packages/react-components/src/renderers/useModelRenderer.ts @@ -27,7 +27,7 @@ export function convertModelRendererArgs, O extends HTMLEl export function useModelRenderer, O extends HTMLElement>( reactRenderer?: ComponentType> | null, - config?: RendererConfig, + config?: RendererConfig>, ): UseRendererResult> { return useRenderer(reactRenderer, convertModelRendererArgs, config); } diff --git a/packages/react-components/src/renderers/useRenderer.ts b/packages/react-components/src/renderers/useRenderer.ts index 3f961089..4f3e55bb 100644 --- a/packages/react-components/src/renderers/useRenderer.ts +++ b/packages/react-components/src/renderers/useRenderer.ts @@ -25,24 +25,25 @@ function rendererReducer( return new Map(state).set(root, args as Slice, 1>); } -export type RendererConfig = { +export type RendererConfig = { renderMode?: 'default' | 'sync' | 'microtask'; + shouldRenderPortal?(root: HTMLElement, ...args: Slice, 1>): boolean; }; export function useRenderer

( node: ReactNode, convert?: (props: Slice, 1>) => PropsWithChildren

, - config?: RendererConfig, + config?: RendererConfig, ): UseRendererResult; export function useRenderer

( reactRenderer: ComponentType

| null | undefined, convert: (props: Slice, 1>) => PropsWithChildren

, - config?: RendererConfig, + config?: RendererConfig, ): UseRendererResult; export function useRenderer

( reactRendererOrNode: ReactNode | ComponentType

| null | undefined, convert?: (props: Slice, 1>) => PropsWithChildren

, - config?: RendererConfig, + config?: RendererConfig, ): UseRendererResult { const [map, update] = useReducer>(rendererReducer, initialState); const renderer = useCallback( @@ -60,14 +61,18 @@ export function useRenderer

( return reactRendererOrNode ? [ - Array.from(map.entries()).map(([root, args]) => - createPortal( - convert - ? createElement

(reactRendererOrNode as ComponentType

, convert(args)) - : (reactRendererOrNode as ReactNode), - root, + Array.from(map.entries()) + .filter(([root, args]) => { + return config?.shouldRenderPortal?.(root, ...args) ?? true; + }) + .map(([root, args]) => + createPortal( + convert + ? createElement

(reactRendererOrNode as ComponentType

, convert(args)) + : (reactRendererOrNode as ReactNode), + root, + ), ), - ), renderer, ] : []; diff --git a/packages/react-components/src/renderers/useSimpleOrChildrenRenderer.ts b/packages/react-components/src/renderers/useSimpleOrChildrenRenderer.ts index 3131987d..91e9c9be 100644 --- a/packages/react-components/src/renderers/useSimpleOrChildrenRenderer.ts +++ b/packages/react-components/src/renderers/useSimpleOrChildrenRenderer.ts @@ -10,7 +10,7 @@ import { export function useSimpleOrChildrenRenderer( fnRenderer?: ComponentType> | null, children?: ReactNode | ComponentType>, - config?: RendererConfig, + config?: RendererConfig>, ): UseRendererResult> { let _children: ReactNode | undefined; let _fnRenderer: ComponentType> | null | undefined; diff --git a/packages/react-components/src/renderers/useSimpleRenderer.ts b/packages/react-components/src/renderers/useSimpleRenderer.ts index d854e104..d40749bb 100644 --- a/packages/react-components/src/renderers/useSimpleRenderer.ts +++ b/packages/react-components/src/renderers/useSimpleRenderer.ts @@ -16,7 +16,7 @@ function convertSimpleRendererArgs([original]: Slice< export function useSimpleRenderer( reactRenderer?: ComponentType> | null, - config?: RendererConfig, + config?: RendererConfig>, ): UseRendererResult> { return useRenderer(reactRenderer, convertSimpleRendererArgs, config); } From 666a13131220abc22229daea5622ce8dd379c0a2 Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Wed, 20 Nov 2024 16:48:03 +0400 Subject: [PATCH 14/29] add more tests --- test/Grid.spec.tsx | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/Grid.spec.tsx b/test/Grid.spec.tsx index 8424dee1..00bf3ba9 100644 --- a/test/Grid.spec.tsx +++ b/test/Grid.spec.tsx @@ -429,6 +429,50 @@ describe('Grid', () => { expect(footerCell).to.have.text('Name Footer'); }); + describe('default renderers', () => { + type GridProItem = { name: string }; + + let items: GridProItem[]; + + function doubleClick(element: Element) { + element.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })); + } + + beforeEach(() => { + items = Array.from(new Array(1)).map((_, i) => ({ name: `name-${i}` })); + }); + + it.only('should update the content', async () => { + render( + items={items}> + path="name" /> + , + ); + + // Get the cell content + let cellContent = await until(() => { + return Array.from(document.querySelectorAll('vaadin-grid-cell-content')).find( + (cellContent) => cellContent.textContent === 'name-0', + ); + }); + doubleClick(cellContent); + const cellEditor = await until(() => document.querySelector('vaadin-grid-pro-edit-text-field')); + // Set a new value + cellEditor.value = 'foo'; + // Exit edit mode + cellEditor.blur(); + // Wait for the editor to close + await until(() => !document.querySelector('vaadin-grid-pro-edit-text-field')); + // Expect the cell content to be connected and have the new value + cellContent = await until(() => { + return Array.from(document.querySelectorAll('vaadin-grid-cell-content')).find( + (cellContent) => cellContent.textContent === 'foo', + ); + }); + expect(cellContent).to.have.text('foo'); + }); + }); + describe('custom renderers', () => { type GridProItem = { name: string }; From 2de1938ee65985d6a474012e45d797caeede365b Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Wed, 20 Nov 2024 16:51:04 +0400 Subject: [PATCH 15/29] run formatter --- test/Grid.spec.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/Grid.spec.tsx b/test/Grid.spec.tsx index 00bf3ba9..7b307c8f 100644 --- a/test/Grid.spec.tsx +++ b/test/Grid.spec.tsx @@ -456,7 +456,9 @@ describe('Grid', () => { ); }); doubleClick(cellContent); - const cellEditor = await until(() => document.querySelector('vaadin-grid-pro-edit-text-field')); + const cellEditor = await until(() => + document.querySelector('vaadin-grid-pro-edit-text-field'), + ); // Set a new value cellEditor.value = 'foo'; // Exit edit mode From 2285dd9fd412bb18d88ca2be35b37e7e5f2250c1 Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Wed, 20 Nov 2024 15:14:24 +0200 Subject: [PATCH 16/29] Update test/Grid.spec.tsx Co-authored-by: Tomi Virkki --- test/Grid.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Grid.spec.tsx b/test/Grid.spec.tsx index 7b307c8f..b39d78b2 100644 --- a/test/Grid.spec.tsx +++ b/test/Grid.spec.tsx @@ -442,7 +442,7 @@ describe('Grid', () => { items = Array.from(new Array(1)).map((_, i) => ({ name: `name-${i}` })); }); - it.only('should update the content', async () => { + it('should update the content', async () => { render( items={items}> path="name" /> From f281097bcd98a9d20aab4ccd521383b0ca8ea7b9 Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Wed, 20 Nov 2024 17:17:38 +0400 Subject: [PATCH 17/29] run formatter --- packages/react-components-pro/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components-pro/package.json b/packages/react-components-pro/package.json index dbda891a..a2193990 100644 --- a/packages/react-components-pro/package.json +++ b/packages/react-components-pro/package.json @@ -153,4 +153,4 @@ "./utils/createComponent.js": "./utils/createComponent.js", "./utils/createComponent.js.map": "./utils/createComponent.js.map" } -} \ No newline at end of file +} From 7a9b3da04128f539b696c6ee46c38d6039fa1253 Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Wed, 20 Nov 2024 17:21:17 +0400 Subject: [PATCH 18/29] run formatter --- packages/react-components/src/Grid.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/src/Grid.tsx b/packages/react-components/src/Grid.tsx index 6de33145..6151d39b 100644 --- a/packages/react-components/src/Grid.tsx +++ b/packages/react-components/src/Grid.tsx @@ -29,7 +29,7 @@ function Grid( ref: ForwardedRef>, ): ReactElement | null { const [portals, rowDetailsRenderer] = useModelRenderer(props.rowDetailsRenderer, { - renderMode: 'microtask' + renderMode: 'microtask', }); const innerRef = useRef(null); From b592c475e21f98a960bf1099edefe02aad1cd6f5 Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Wed, 20 Nov 2024 17:22:20 +0400 Subject: [PATCH 19/29] revert unintentional changes --- packages/react-components-pro/package.json | 2 +- packages/react-components/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-components-pro/package.json b/packages/react-components-pro/package.json index a2193990..dbda891a 100644 --- a/packages/react-components-pro/package.json +++ b/packages/react-components-pro/package.json @@ -153,4 +153,4 @@ "./utils/createComponent.js": "./utils/createComponent.js", "./utils/createComponent.js.map": "./utils/createComponent.js.map" } -} +} \ No newline at end of file diff --git a/packages/react-components/package.json b/packages/react-components/package.json index a1554d09..f2c24b9a 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -538,4 +538,4 @@ "./renderers/useSimpleRenderer.js": "./renderers/useSimpleRenderer.js", "./renderers/useSimpleRenderer.js.map": "./renderers/useSimpleRenderer.js.map" } -} \ No newline at end of file +} From bf8c7b985abfef2ba473a4b006d552eb5ccd0e9b Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Wed, 20 Nov 2024 17:25:04 +0400 Subject: [PATCH 20/29] changes to package.json after building locally --- packages/react-components-pro/package.json | 2 -- packages/react-components/package.json | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/react-components-pro/package.json b/packages/react-components-pro/package.json index dbda891a..12f973a2 100644 --- a/packages/react-components-pro/package.json +++ b/packages/react-components-pro/package.json @@ -148,8 +148,6 @@ "./GridProEditColumn": "./GridProEditColumn.js", "./Map": "./Map.js", "./RichTextEditor": "./RichTextEditor.js", - "./utils/createComponent.d.ts": "./utils/createComponent.d.ts", - "./utils/createComponent.d.ts.map": "./utils/createComponent.d.ts.map", "./utils/createComponent.js": "./utils/createComponent.js", "./utils/createComponent.js.map": "./utils/createComponent.js.map" } diff --git a/packages/react-components/package.json b/packages/react-components/package.json index f2c24b9a..a1554d09 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -538,4 +538,4 @@ "./renderers/useSimpleRenderer.js": "./renderers/useSimpleRenderer.js", "./renderers/useSimpleRenderer.js.map": "./renderers/useSimpleRenderer.js.map" } -} +} \ No newline at end of file From 5a8d15bcb34c4ac1fbf7b7dd6312113f0d948842 Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Wed, 20 Nov 2024 17:26:33 +0400 Subject: [PATCH 21/29] Revert "changes to package.json after building locally" This reverts commit 4818b227f7d5e975fe92a90be437603b6290bc59. --- packages/react-components-pro/package.json | 2 ++ packages/react-components/package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react-components-pro/package.json b/packages/react-components-pro/package.json index 12f973a2..dbda891a 100644 --- a/packages/react-components-pro/package.json +++ b/packages/react-components-pro/package.json @@ -148,6 +148,8 @@ "./GridProEditColumn": "./GridProEditColumn.js", "./Map": "./Map.js", "./RichTextEditor": "./RichTextEditor.js", + "./utils/createComponent.d.ts": "./utils/createComponent.d.ts", + "./utils/createComponent.d.ts.map": "./utils/createComponent.d.ts.map", "./utils/createComponent.js": "./utils/createComponent.js", "./utils/createComponent.js.map": "./utils/createComponent.js.map" } diff --git a/packages/react-components/package.json b/packages/react-components/package.json index a1554d09..f2c24b9a 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -538,4 +538,4 @@ "./renderers/useSimpleRenderer.js": "./renderers/useSimpleRenderer.js", "./renderers/useSimpleRenderer.js.map": "./renderers/useSimpleRenderer.js.map" } -} \ No newline at end of file +} From 5450f35e550c4356d890945d8a4fb8ca48afa193 Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Wed, 20 Nov 2024 17:40:03 +0400 Subject: [PATCH 22/29] fix import --- packages/react-components-pro/src/GridProEditColumn.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-components-pro/src/GridProEditColumn.tsx b/packages/react-components-pro/src/GridProEditColumn.tsx index 60c403d7..ba2f0808 100644 --- a/packages/react-components-pro/src/GridProEditColumn.tsx +++ b/packages/react-components-pro/src/GridProEditColumn.tsx @@ -8,9 +8,10 @@ * See https://vaadin.com/commercial-license-and-service-terms for the full * license. */ -import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'; +import React, { useLayoutEffect, useRef, useState } from 'react'; import { type ForwardedRef, forwardRef, type ReactElement, type ReactNode, type RefAttributes } from 'react'; -import type { GridBodyRenderer, GridDefaultItem } from '@vaadin/react-components/Grid.js'; +import { flushSync } from 'react-dom'; +import type { GridDefaultItem } from '@vaadin/react-components/Grid.js'; import type { GridColumnProps } from '@vaadin/react-components/GridColumn.js'; import { GridProEditColumn as _GridProEditColumn, @@ -20,8 +21,7 @@ import { import { useModelRenderer } from '@vaadin/react-components/renderers/useModelRenderer.js'; import { useSimpleOrChildrenRenderer } from '@vaadin/react-components/renderers/useSimpleOrChildrenRenderer.js'; import type { OmittedGridColumnHTMLAttributes } from '@vaadin/react-components/GridColumn.js'; -import useMergedRefs from '../../react-components/src/utils/useMergedRefs.js'; -import { flushSync } from 'react-dom'; +import useMergedRefs from '@vaadin/react-components/utils/useMergedRefs.js'; export * from './generated/GridProEditColumn.js'; From 6cb6892d0db7eeee49bc0e7210f9fad6379980cd Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Wed, 20 Nov 2024 18:48:33 +0400 Subject: [PATCH 23/29] remove last blank line in package.json --- packages/react-components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/package.json b/packages/react-components/package.json index f2c24b9a..a1554d09 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -538,4 +538,4 @@ "./renderers/useSimpleRenderer.js": "./renderers/useSimpleRenderer.js", "./renderers/useSimpleRenderer.js.map": "./renderers/useSimpleRenderer.js.map" } -} +} \ No newline at end of file From d01f67cc11f2655a480495efadedd2052d7a91f1 Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Thu, 21 Nov 2024 10:24:59 +0400 Subject: [PATCH 24/29] explain method overrides in GridPro --- .../src/GridProEditColumn.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/react-components-pro/src/GridProEditColumn.tsx b/packages/react-components-pro/src/GridProEditColumn.tsx index ba2f0808..a2cbbfa9 100644 --- a/packages/react-components-pro/src/GridProEditColumn.tsx +++ b/packages/react-components-pro/src/GridProEditColumn.tsx @@ -57,9 +57,9 @@ export type GridProEditColumnProps = Partial< }>; type GridProEditColumnElementInternals = { + _clearCellContent(cell: HTMLElement & { [SKIP_CLEARING_CELL_CONTENT]?: boolean }): void; _renderEditor(cell: HTMLElement & { [SKIP_CLEARING_CELL_CONTENT]?: boolean }, model: { item: TItem }): void; _removeEditor(cell: HTMLElement & { [SKIP_CLEARING_CELL_CONTENT]?: boolean }, model: { item: TItem }): void; - _clearCellContent(cell: HTMLElement & { [SKIP_CLEARING_CELL_CONTENT]?: boolean }): void; }; const SKIP_CLEARING_CELL_CONTENT = Symbol(); @@ -71,6 +71,7 @@ function GridProEditColumn( const [editedItem, setEditedItem] = useState(null); const [editModePortals, editModeRenderer] = useModelRenderer(props.editModeRenderer, { + // The web component implementation currently requires the editor to be rendered synchronously. renderMode: 'sync', shouldRenderPortal: (_root, _column, model) => editedItem === model.item, }); @@ -90,6 +91,8 @@ function GridProEditColumn( useLayoutEffect(() => { innerRef.current!._clearCellContent = function (cell) { + // Clearing cell content in _renderEditor and _removeEditor is decided + // based on whether the content was rendered by a React renderer or not. if (!cell[SKIP_CLEARING_CELL_CONTENT]) { Object.getPrototypeOf(this)._clearCellContent.call(this, cell); } @@ -98,10 +101,14 @@ function GridProEditColumn( useLayoutEffect(() => { innerRef.current!._renderEditor = function (cell, model) { + // Ensure the corresponding bodyRenderer portal is removed and the editModeRenderer portal + // is added instead. flushSync(() => { setEditedItem(model.item); }); + // Manually clear the cell content only if it was rendered by the default grid renderer. + // For content rendered by a React renderer, clearing is handled by removing the portal. if (!bodyRenderer) { this._clearCellContent(cell); } @@ -114,10 +121,16 @@ function GridProEditColumn( useLayoutEffect(() => { innerRef.current!._removeEditor = function (cell, model) { + // Manually clear the cell content only if it was rendered by the default grid renderer. + // For content rendered by a React renderer, clearing is handled by removing the portal. if (!editModeRenderer) { this._clearCellContent(cell); } + // Ensure the editModeRenderer portal is removed and the corresponding bodyRenderer portal + // is added again. Please note the bodyRenderer portal will be added synchronously even though + // the renderer has renderMode set to microtask. It's because the portal already has content + // from the previous render cycle and we just show it again. flushSync(() => { setEditedItem((editedItem) => { return editedItem === model.item ? null : editedItem; From e415bb22159910d3d9eca31d88ceb8134e1c05ed Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Thu, 21 Nov 2024 10:44:02 +0400 Subject: [PATCH 25/29] fix another bug --- .../src/GridProEditColumn.tsx | 12 ++-- test/Grid.spec.tsx | 63 +++++++++++++++++++ 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/packages/react-components-pro/src/GridProEditColumn.tsx b/packages/react-components-pro/src/GridProEditColumn.tsx index a2cbbfa9..b86a022e 100644 --- a/packages/react-components-pro/src/GridProEditColumn.tsx +++ b/packages/react-components-pro/src/GridProEditColumn.tsx @@ -101,18 +101,18 @@ function GridProEditColumn( useLayoutEffect(() => { innerRef.current!._renderEditor = function (cell, model) { - // Ensure the corresponding bodyRenderer portal is removed and the editModeRenderer portal - // is added instead. - flushSync(() => { - setEditedItem(model.item); - }); - // Manually clear the cell content only if it was rendered by the default grid renderer. // For content rendered by a React renderer, clearing is handled by removing the portal. if (!bodyRenderer) { this._clearCellContent(cell); } + // Ensure the corresponding bodyRenderer portal is removed and the editModeRenderer portal + // is added instead. + flushSync(() => { + setEditedItem(model.item); + }); + cell[SKIP_CLEARING_CELL_CONTENT] = true; Object.getPrototypeOf(this)._renderEditor.call(this, cell, model); cell[SKIP_CLEARING_CELL_CONTENT] = false; diff --git a/test/Grid.spec.tsx b/test/Grid.spec.tsx index b39d78b2..efe2b340 100644 --- a/test/Grid.spec.tsx +++ b/test/Grid.spec.tsx @@ -536,6 +536,26 @@ describe('Grid', () => { expect(cellContent.isConnected).to.be.false; }); + it('should toggle edit mode on double click', async () => { + render( + items={items}> + path="name" editModeRenderer={() => }> + {({ item }) => {item.name}} + + , + ); + + for (let i = 0; i < 2; i++) { + const cellContent = await until(() => document.querySelector('.content')); + doubleClick(cellContent); + + const cellEditor = await until(() => document.querySelector('.editor')); + focusOut(cellEditor); + + await until(() => !document.querySelector('.editor')); + } + }); + it('should have updated content', async () => { render( items={items}> @@ -561,6 +581,29 @@ describe('Grid', () => { expect(cellContent).to.have.text('foo'); }); + it('should toggle edit mode on double click without a custom renderer', async () => { + render( + items={items}> + + path="name" + renderer={({ item }) => {item.name}} + /> + , + ); + + for (let i = 0; i < 2; i++) { + const cellContent = await until(() => document.querySelector('.content')); + doubleClick(cellContent); + + const cellEditor = await until(() => + document.querySelector('vaadin-grid-pro-edit-text-field'), + ); + focusOut(cellEditor); + + await until(() => !document.querySelector('input')); + } + }); + it('should update the content without a custom editor', async () => { render( items={items}> @@ -586,6 +629,26 @@ describe('Grid', () => { expect(cellContent).to.have.text('foo'); }); + it('should toggle edit mode on double click without a custom renderer', async () => { + render( + items={items}> + path="name" editModeRenderer={() => } /> + , + ); + + for (let i = 0; i < 2; i++) { + const cellContent = await until(() => + Array.from(document.querySelectorAll('vaadin-grid-cell-content')).find((c) => c.textContent === 'name-0'), + ); + doubleClick(cellContent!); + + const cellEditor = await until(() => document.querySelector('.editor')); + focusOut(cellEditor); + + await until(() => !document.querySelector('.editor')); + } + }); + it('should update the content without a custom renderer', async () => { render( items={items}> From d961d2ac638cf8f917c88bc0f0920bce1ad401fe Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Thu, 21 Nov 2024 10:50:52 +0400 Subject: [PATCH 26/29] explain yet another method override --- packages/react-components/src/Grid.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-components/src/Grid.tsx b/packages/react-components/src/Grid.tsx index 6151d39b..7a761d03 100644 --- a/packages/react-components/src/Grid.tsx +++ b/packages/react-components/src/Grid.tsx @@ -37,6 +37,7 @@ function Grid( useLayoutEffect(() => { innerRef.current!.recalculateColumnWidths = function (...args) { + // Wait for column content to finish rendering before recalculating widths. queueMicrotask(() => { Object.getPrototypeOf(this).recalculateColumnWidths.call(this, ...args); }); From c6e05c5bc3ffa833a87a9430ac2077c5cdfda338 Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Thu, 21 Nov 2024 10:54:30 +0400 Subject: [PATCH 27/29] fix type in test name --- test/Grid.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Grid.spec.tsx b/test/Grid.spec.tsx index efe2b340..feb954a1 100644 --- a/test/Grid.spec.tsx +++ b/test/Grid.spec.tsx @@ -581,7 +581,7 @@ describe('Grid', () => { expect(cellContent).to.have.text('foo'); }); - it('should toggle edit mode on double click without a custom renderer', async () => { + it('should toggle edit mode on double click without a custom editor', async () => { render( items={items}> From 840516350ccfec62236d97f96bb45ee8be20f552 Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Thu, 21 Nov 2024 14:31:04 +0400 Subject: [PATCH 28/29] improve test coverage --- test/Grid.spec.tsx | 97 +++++++++++++++++++++++++++++++++------------- 1 file changed, 69 insertions(+), 28 deletions(-) diff --git a/test/Grid.spec.tsx b/test/Grid.spec.tsx index feb954a1..5248138b 100644 --- a/test/Grid.spec.tsx +++ b/test/Grid.spec.tsx @@ -439,7 +439,34 @@ describe('Grid', () => { } beforeEach(() => { - items = Array.from(new Array(1)).map((_, i) => ({ name: `name-${i}` })); + items = [{ name: 'name-0' }]; + }); + + it('should toggle edit mode on double click', async () => { + render( + items={items}> + path="name" /> + , + ); + + const cellContent = await until(() => + Array.from(document.querySelectorAll('vaadin-grid-cell-content')).find( + (cellContent) => cellContent.textContent === 'name-0', + ), + ); + + for (let i = 0; i < 2; i++) { + expect(cellContent.textContent?.trim()).to.equal('name-0'); + doubleClick(cellContent); + + const cellEditor = await until(() => + cellContent.querySelector('vaadin-grid-pro-edit-text-field'), + ); + expect(cellContent.textContent?.trim()).to.be.empty; + cellEditor.blur(); + + await until(() => !cellContent.contains(cellEditor)); + } }); it('should update the content', async () => { @@ -450,28 +477,23 @@ describe('Grid', () => { ); // Get the cell content - let cellContent = await until(() => { - return Array.from(document.querySelectorAll('vaadin-grid-cell-content')).find( + const cellContent = await until(() => + Array.from(document.querySelectorAll('vaadin-grid-cell-content')).find( (cellContent) => cellContent.textContent === 'name-0', - ); - }); + ), + ); doubleClick(cellContent); const cellEditor = await until(() => - document.querySelector('vaadin-grid-pro-edit-text-field'), + cellContent.querySelector('vaadin-grid-pro-edit-text-field'), ); // Set a new value cellEditor.value = 'foo'; // Exit edit mode cellEditor.blur(); // Wait for the editor to close - await until(() => !document.querySelector('vaadin-grid-pro-edit-text-field')); + await until(() => !cellContent.contains(cellEditor)); // Expect the cell content to be connected and have the new value - cellContent = await until(() => { - return Array.from(document.querySelectorAll('vaadin-grid-cell-content')).find( - (cellContent) => cellContent.textContent === 'foo', - ); - }); - expect(cellContent).to.have.text('foo'); + await until(() => cellContent.textContent === 'foo'); }); }); @@ -545,14 +567,21 @@ describe('Grid', () => { , ); + const cellContent = await until(() => + Array.from(document.querySelectorAll('vaadin-grid-cell-content')).find((cellContent) => + cellContent.querySelector('.content'), + ), + ); + for (let i = 0; i < 2; i++) { - const cellContent = await until(() => document.querySelector('.content')); + expect(cellContent.textContent?.trim()).to.equal('name-0'); doubleClick(cellContent); - const cellEditor = await until(() => document.querySelector('.editor')); - focusOut(cellEditor); + const cellEditor = await until(() => cellContent.querySelector('.editor')); + expect(cellContent.textContent?.trim()).to.be.empty; + cellEditor.blur(); - await until(() => !document.querySelector('.editor')); + await until(() => !cellContent.contains(cellEditor)); } }); @@ -591,16 +620,23 @@ describe('Grid', () => { , ); + const cellContent = await until(() => + Array.from(document.querySelectorAll('vaadin-grid-cell-content')).find((cellContent) => + cellContent.querySelector('.content'), + ), + ); + for (let i = 0; i < 2; i++) { - const cellContent = await until(() => document.querySelector('.content')); + expect(cellContent.textContent?.trim()).to.equal('name-0'); doubleClick(cellContent); const cellEditor = await until(() => - document.querySelector('vaadin-grid-pro-edit-text-field'), + cellContent.querySelector('vaadin-grid-pro-edit-text-field'), ); - focusOut(cellEditor); + expect(cellContent.textContent?.trim()).to.be.empty; + cellEditor.blur(); - await until(() => !document.querySelector('input')); + await until(() => !cellContent.contains(cellEditor)); } }); @@ -636,16 +672,21 @@ describe('Grid', () => { , ); + const cellContent = await until(() => + Array.from(document.querySelectorAll('vaadin-grid-cell-content')).find( + (cellContent) => cellContent.textContent === 'name-0', + ), + ); + for (let i = 0; i < 2; i++) { - const cellContent = await until(() => - Array.from(document.querySelectorAll('vaadin-grid-cell-content')).find((c) => c.textContent === 'name-0'), - ); - doubleClick(cellContent!); + expect(cellContent.textContent?.trim()).to.equal('name-0'); + doubleClick(cellContent); - const cellEditor = await until(() => document.querySelector('.editor')); - focusOut(cellEditor); + const cellEditor = await until(() => cellContent.querySelector('.editor')); + expect(cellContent.textContent?.trim()).to.equal(''); + cellEditor.blur(); - await until(() => !document.querySelector('.editor')); + await until(() => !cellContent.contains(cellEditor)); } }); From 9199d2fd499aeae328176e32d150a2acaee94a0b Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Thu, 21 Nov 2024 14:39:11 +0400 Subject: [PATCH 29/29] reduce code repetition in tests --- test/Grid.spec.tsx | 159 ++++++++++++++++----------------------------- 1 file changed, 56 insertions(+), 103 deletions(-) diff --git a/test/Grid.spec.tsx b/test/Grid.spec.tsx index 5248138b..732b85e8 100644 --- a/test/Grid.spec.tsx +++ b/test/Grid.spec.tsx @@ -54,27 +54,28 @@ describe('Grid', () => { return <>{item.name}; } - function isGridCellContentNodeRendered(node: Node) { - return ( - node instanceof Text && - node.parentNode instanceof HTMLElement && - node.parentNode.localName === 'vaadin-grid-cell-content' - ); - } - - function getGridMeaningfulParts(columnElementName: string) { - const grid = document.querySelector('vaadin-grid, vaadin-grid-pro')!; - expect(grid).to.exist; - - const columns = document.querySelectorAll(columnElementName); + function getGridMeaningfulParts( + columnElementName: string, + assertions: { expectedColumnCount: number; expectedCellCount: number }, + ) { + return waitFor(async () => { + const grid = document.querySelector('vaadin-grid, vaadin-grid-pro')!; + expect(grid).to.exist; + + const columns = document.querySelectorAll(columnElementName); + + // Filter cells that don't have any textContent. Grid creates empty cells for some calculations, + // but we don't need them. + const cells = Array.from(grid!.querySelectorAll('vaadin-grid-cell-content')).filter( + ({ textContent }) => textContent, + ); - // Filter cells that don't have any textContent. Grid creates empty cells for some calculations, - // but we don't need them. - const cells = Array.from(grid!.querySelectorAll('vaadin-grid-cell-content')).filter( - ({ textContent }) => textContent, - ); + const { expectedColumnCount, expectedCellCount } = assertions; + expect(columns).to.have.lengthOf(expectedColumnCount); + expect(cells).to.have.lengthOf(expectedCellCount); - return [columns, cells] as const; + return [columns, cells] as const; + }); } afterEach(cleanup); @@ -99,13 +100,9 @@ describe('Grid', () => { , ); - const [_columns, cells] = await waitFor(async () => { - const [columns, cells] = getGridMeaningfulParts('vaadin-grid-column'); - - expect(columns).to.have.length(3); - expect(cells).to.have.length(15); - - return [columns, cells]; + const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-column', { + expectedColumnCount: 3, + expectedCellCount: 15, }); const [headerRendererCell, headerInlineCell, nameHeaderCell, surnameHeaderCell, roleHeaderCell] = cells.slice( @@ -218,13 +215,9 @@ describe('Grid', () => { , ); - const [_columns, cells] = await waitFor(async () => { - const [columns, cells] = getGridMeaningfulParts('vaadin-grid-column'); - - expect(columns).to.have.length(1); - expect(cells).to.have.length(6); - - return [columns, cells]; + const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-column', { + expectedColumnCount: 1, + expectedCellCount: 6, }); const [groupHeaderCell, nameHeaderCell, nameFooterCell, groupFooterCell] = cells; @@ -244,13 +237,9 @@ describe('Grid', () => { , ); - const [_columns, cells] = await waitFor(async () => { - const [columns, cells] = getGridMeaningfulParts('vaadin-grid-filter-column'); - - expect(columns).to.have.length(1); - expect(cells).to.have.length(3); - - return [columns, cells]; + const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-filter-column', { + expectedColumnCount: 1, + expectedCellCount: 3, }); const [footerCell, bodyCell1, bodyCell2] = cells; @@ -267,13 +256,9 @@ describe('Grid', () => { , ); - const [_columns, cells] = await waitFor(async () => { - const [columns, cells] = getGridMeaningfulParts('vaadin-grid-filter-column'); - - expect(columns).to.have.length(1); - expect(cells).to.have.length(4); - - return [columns, cells]; + const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-filter-column', { + expectedColumnCount: 1, + expectedCellCount: 4, }); const footerCell = cells[1]; @@ -292,13 +277,9 @@ describe('Grid', () => { , ); - const [_columns, cells] = await waitFor(async () => { - const [columns, cells] = getGridMeaningfulParts('vaadin-grid-selection-column'); - - expect(columns).to.have.length(1); - expect(cells).to.have.length(4); - - return [columns, cells]; + const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-selection-column', { + expectedColumnCount: 1, + expectedCellCount: 4, }); const [headerCell, footerCell, bodyCell1, bodyCell2] = cells; @@ -318,13 +299,9 @@ describe('Grid', () => { , ); - const [_columns, cells] = await waitFor(async () => { - const [columns, cells] = getGridMeaningfulParts('vaadin-grid-selection-column'); - - expect(columns).to.have.length(1); - expect(cells).to.have.length(4); - - return [columns, cells]; + const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-selection-column', { + expectedColumnCount: 1, + expectedCellCount: 4, }); const [headerCell, footerCell] = cells; @@ -342,13 +319,9 @@ describe('Grid', () => { , ); - const [_columns, cells] = await waitFor(async () => { - const [columns, cells] = getGridMeaningfulParts('vaadin-grid-sort-column'); - - expect(columns).to.have.length(1); - expect(cells).to.have.length(3); - - return [columns, cells]; + const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-sort-column', { + expectedColumnCount: 1, + expectedCellCount: 3, }); const [footerCell, bodyCell1, bodyCell2] = cells; @@ -365,13 +338,9 @@ describe('Grid', () => { , ); - const [_columns, cells] = await waitFor(async () => { - const [columns, cells] = getGridMeaningfulParts('vaadin-grid-sort-column'); - - expect(columns).to.have.length(1); - expect(cells).to.have.length(4); - - return [columns, cells]; + const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-sort-column', { + expectedColumnCount: 1, + expectedCellCount: 4, }); const footerCell = cells[1]; @@ -390,13 +359,9 @@ describe('Grid', () => { , ); - const [_columns, cells] = await waitFor(async () => { - const [columns, cells] = getGridMeaningfulParts('vaadin-grid-pro-edit-column'); - - expect(columns).to.have.length(1); - expect(cells).to.have.length(4); - - return [columns, cells]; + const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-pro-edit-column', { + expectedColumnCount: 1, + expectedCellCount: 4, }); const [headerCell, footerCell, bodyCell1, bodyCell2] = cells; @@ -414,13 +379,9 @@ describe('Grid', () => { , ); - const [_columns, cells] = await waitFor(async () => { - const [columns, cells] = getGridMeaningfulParts('vaadin-grid-pro-edit-column'); - - expect(columns).to.have.length(1); - expect(cells).to.have.length(4); - - return [columns, cells]; + const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-pro-edit-column', { + expectedColumnCount: 1, + expectedCellCount: 4, }); const [headerCell, footerCell] = cells; @@ -780,13 +741,9 @@ describe('Grid', () => { , ); - const [_columns, cells] = await waitFor(async () => { - const [columns, cells] = getGridMeaningfulParts('vaadin-grid-tree-column'); - - expect(columns).to.have.length(1); - expect(cells).to.have.length(7); - - return [columns, cells]; + const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-tree-column', { + expectedColumnCount: 1, + expectedCellCount: 7, }); const [treeHeaderCell, nameHeaderCell, treeFooterCell] = cells; @@ -804,13 +761,9 @@ describe('Grid', () => { , ); - const [_columns, cells] = await waitFor(async () => { - const [columns, cells] = getGridMeaningfulParts('vaadin-grid-tree-column'); - - expect(columns).to.have.length(1); - expect(cells).to.have.length(7); - - return [columns, cells]; + const [columns, cells] = await getGridMeaningfulParts('vaadin-grid-tree-column', { + expectedColumnCount: 1, + expectedCellCount: 7, }); const [treeHeaderCell, nameHeaderCell, treeFooterCell] = cells;