bvaughn / react-window

React components for efficiently rendering large lists and tabular data
https://react-window.now.sh/
MIT License
15.91k stars 787 forks source link

How to overlay elements at the same coordinates of grid cells in FixedSizeGrid? #607

Closed scottyantipa closed 2 months ago

scottyantipa commented 2 years ago

Thanks for the great lib. I have a use case where I'm building a spreadsheet-like application. It's a flowchart editor but constrained to a grid, you can see it here. I need to overlay some elements such that they line up with cells in the grid. For example, to edit the text in a cell I render a <textarea /> absolutely positioned over the underlying cell. It needs to be perfectly aligned to preserve the illusion that you're directly editing the text in the cell. I also have to render some svg lines between nodes in the flowchart, etc. When scrolling the overlay items also need to stay positioned correctly relative to the cells.

I'm currently achieving this (see link above) through a hack of getting a ref to FixedSizeGrid's innerRef and then using React.createPortal to render the overlay elements into the same parent container as the cells. This gives the overlay items the same origin as the cells so they line up by default. This works, but of course is a hack and depends on some assumptions about the internals of react-window.

My question is: Is there a more standard approach to this? I have seen discussions using ScrollSync, which I tried to implement without much success. There was too much lag / perf issues. I thought about modeling the overlay elements as grid items themselves but couldn't quite figure this out, especially for the <svg> element that renders the lines between nodes.

Screenshots (or inspect element on knotend.com): Screen Shot 2021-11-20 at 8 05 54 AM

Screen Shot 2021-11-20 at 8 11 15 AM

And the code looks roughly like this:

...Component =...
    // Get a ref to FixedSizeGrid's inner element and use it
    // to React.createPortal later, rendering the overlay elements.
    const tableInnerElementRef = useRef();

    return (
        <div
            style={{ position: 'relative', display: 'flex' }}
        >
            <div style={{ flex: '1 1 auto' }}>
                <AutoSizer>
                    {
                        ({ width, height }) => {
                            return (
                                <div style={{ position: 'absolute', top: 0, left: 0, width, height }}>
                                    <FixedSizeGrid
                                        itemData={data}
                                        ref={setTableRef}
                                        innerRef={(r) =>{
                                            if (r) {
                                                r.classList.add("grid-inner-ref-override");
                                                if (tableInnerElementRef.current != r) {
                                                    tableInnerElementRef.current = r;
                                                    forceUpdate();
                                                }
                                            }
                                        }}
                                        rowCount={rowCount}
                                        columnCount={colCount}
                                        columnWidth={cellWidth + cellPadding}
                                        rowHeight={cellHeight + cellPadding}
                                        width={width}
                                        height={height}
                                    >
                                        {VirtualCell}
                                    </FixedSizeGrid>
                                    {tableInnerElementRef.current && ReactDOM.createPortal(
                                        <React.Fragment>
                                            <NodeConnections graph={graph} cellWidth={cellWidth} cellHeight={cellHeight} cellPadding={cellPadding} />
                                            {
                                                editing ? renderInput() : null
                                            }
                                            {selectionBoxes.length > 0 ? selectionBoxes : null}
                                            {activeCellBox}
                                        </React.Fragment>,
                                        tableInnerElementRef.current
                                    )}
                                </div>
                            )
                        }
                    }
                </AutoSizer>
            </div>
        </div>
    );

Thanks again!

pedromoter commented 1 year ago

I am working with something similar but overscan keeps deleting my overlay cause they are larger than the origin.

Did portals fix it ?

scottyantipa commented 1 year ago

@pedromoter yep, React.createPortal is working well