teableio/teable

View on GitHub
packages/sdk/src/components/grid/InteractionLayer.tsx

Summary

Maintainability
F
6 days
Test Coverage
/* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
import { isEqual } from 'lodash';
import type { Dispatch, ForwardRefRenderFunction, SetStateAction } from 'react';
import { useState, useRef, forwardRef, useImperativeHandle, useMemo, useLayoutEffect } from 'react';
import { useClickAway, useMouse } from 'react-use';
import type { CellScrollerRef } from './CellScroller';
import { CellScroller } from './CellScroller';
import type { IEditorContainerRef } from './components';
import { EditorContainer } from './components';
import type { IGridTheme } from './configs';

import {
  GRID_DEFAULT,
  DEFAULT_MOUSE_STATE,
  DEFAULT_DRAG_STATE,
  DEFAULT_COLUMN_RESIZE_STATE,
} from './configs';
import type { IGridProps } from './Grid';
import {
  useSelection,
  useAutoScroll,
  useColumnResize,
  useColumnFreeze,
  useEventListener,
} from './hooks';
import { useDrag } from './hooks/useDrag';
import { useVisibleRegion } from './hooks/useVisibleRegion';
import type {
  IActiveCellBound,
  ICellItem,
  ICellPosition,
  ICellRegionWithData,
  IInnerCell,
  ILinearRow,
  IMouseState,
  IRowControlItem,
  IScrollState,
} from './interface';
import {
  RegionType,
  LinearRowType,
  DragRegionType,
  MouseButtonType,
  SelectionRegionType,
  DraggableType,
  SelectableType,
} from './interface';
import type { CoordinateManager, ImageManager, SpriteManager, CombinedSelection } from './managers';
import { CellRegionType, getCellRenderer } from './renderers';
import { RenderLayer } from './RenderLayer';
import type { IRegionData } from './utils';
import { BLANK_REGION_DATA, flatRanges, getRegionData, inRange } from './utils';

const { columnAppendBtnWidth, columnHeadHeight } = GRID_DEFAULT;

export interface IInteractionLayerProps
  extends Omit<
    IGridProps,
    | 'freezeColumnCount'
    | 'rowCount'
    | 'rowHeight'
    | 'style'
    | 'smoothScrollX'
    | 'smoothScrollY'
    | 'onVisibleRegionChanged'
  > {
  theme: IGridTheme;
  width: number;
  height: number;
  forceRenderFlag: string;
  rowControls: IRowControlItem[];
  mouseState: IMouseState;
  scrollState: IScrollState;
  imageManager: ImageManager;
  spriteManager: SpriteManager;
  coordInstance: CoordinateManager;
  activeCell: ICellItem | null;
  activeCellBound: IActiveCellBound | null;
  real2RowIndex: (index: number) => number;
  getLinearRow: (index: number) => ILinearRow;
  setActiveCell: Dispatch<SetStateAction<ICellItem | null>>;
  setMouseState: Dispatch<SetStateAction<IMouseState>>;
  scrollBy: (deltaX: number, deltaY: number) => void;
  scrollToItem: (position: [columnIndex: number, rowIndex: number]) => void;
}

export interface IInteractionLayerRef {
  resetState: () => void;
  setSelection: (selection: CombinedSelection) => void;
}

export const InteractionLayerBase: ForwardRefRenderFunction<
  IInteractionLayerRef,
  IInteractionLayerProps
> = (props, ref) => {
  const {
    theme,
    width,
    height,
    columns,
    commentCountMap,
    draggable,
    selectable,
    rowControls,
    mouseState,
    scrollState,
    imageManager,
    spriteManager,
    coordInstance,
    columnStatistics,
    forceRenderFlag,
    rowIndexVisible,
    groupCollection,
    isMultiSelectionEnable,
    activeCellBound: _activeCellBound,
    columnHeaderVisible,
    collapsedGroupIds,
    collaborators,
    activeCell,
    getLinearRow,
    real2RowIndex,
    setActiveCell,
    setMouseState,
    scrollToItem,
    scrollBy,
    getCellContent,
    onUndo,
    onRedo,
    onCopy,
    onPaste,
    onDelete,
    onRowAppend,
    onRowExpand,
    onRowOrdered,
    onCellEdited,
    onSelectionChanged,
    onColumnFreeze,
    onColumnAppend,
    onColumnResize,
    onColumnOrdered,
    onContextMenu,
    onItemHovered,
    onItemClick,
    onColumnHeaderClick,
    onColumnHeaderDblClick,
    onColumnHeaderMenuClick,
    onColumnStatisticClick,
    onCollapsedGroupChanged,
    onDragStart: _onDragStart,
  } = props;

  useImperativeHandle(ref, () => ({
    resetState,
    setSelection: (selection: CombinedSelection) => {
      const { type, ranges } = selection;

      switch (type) {
        case SelectionRegionType.Cells: {
          const activeCell = ranges[0];
          setActiveCell(activeCell);
          scrollToItem(activeCell);
          break;
        }
        case SelectionRegionType.Columns: {
          const activeCell = [ranges[0][0], 0] as ICellItem;
          setActiveCell(activeCell);
          scrollToItem(activeCell);
          break;
        }
        default: {
          setActiveCell(null);
          break;
        }
      }
      setSelection(selection);
    },
  }));

  const stageRef = useRef<HTMLDivElement | null>(null);
  const containerRef = useRef<HTMLDivElement | null>(null);
  const editorContainerRef = useRef<IEditorContainerRef>(null);
  const cellScrollerRef = useRef<CellScrollerRef | null>(null);
  const prevActiveCellRef = useRef<ICellItem | null>(null);
  const hoveredRegionRef = useRef<IRegionData>(BLANK_REGION_DATA);
  const previousHoveredRegionRef = useRef<IRegionData>(BLANK_REGION_DATA);

  const mousePosition = useMouse(stageRef);
  const [cellScrollTop, setCellScrollTop] = useState(0);
  const [hoverCellPosition, setHoverCellPosition] = useState<ICellPosition | null>(null);
  const [cursor, setCursor] = useState('default');
  const [isEditing, setEditing] = useState(false);

  const { containerHeight, freezeColumnCount } = coordInstance;
  const { scrollTop, scrollLeft, isScrolling } = scrollState;
  const { type: regionType } = mouseState;
  const isRowAppendEnable = onRowAppend != null;
  const isColumnFreezable = onColumnFreeze != null;
  const isColumnResizable = onColumnResize != null;
  const isColumnAppendEnable = onColumnAppend != null;
  const isColumnHeaderMenuVisible = onColumnHeaderMenuClick != null;

  const visibleRegion = useVisibleRegion(coordInstance, scrollState, forceRenderFlag);
  const {
    columnResizeState,
    hoveredColumnResizeIndex,
    setHoveredColumnResizeIndex,
    setColumnResizeState,
    onColumnResizeStart,
    onColumnResizeChange,
    onColumnResizeEnd,
  } = useColumnResize(coordInstance, scrollState);
  const {
    selection,
    isSelecting,
    setSelection,
    onSelectionStart,
    onSelectionChange,
    onSelectionEnd,
    onSelectionClick,
    onSelectionContextMenu,
  } = useSelection({
    selectable,
    coordInstance,
    isMultiSelectionEnable,
    getLinearRow,
    setActiveCell,
    onSelectionChanged,
  });
  const { dragState, setDragState, onDragStart, onDragChange, onDragEnd } = useDrag(
    coordInstance,
    scrollState,
    selection,
    draggable
  );
  const { columnFreezeState, onColumnFreezeStart, onColumnFreezeMove, onColumnFreezeEnd } =
    useColumnFreeze(coordInstance, scrollState);

  const { isDragging, type: dragType } = dragState;
  const { isFreezing } = columnFreezeState;
  const isResizing = columnResizeState.columnIndex > -1;
  const { isCellSelection, ranges: selectionRanges } = selection;
  const isInteracting = isSelecting || isDragging || isResizing || isFreezing;
  const [activeColumnIndex, activeRowIndex] = activeCell ?? [];

  const getPosition = () => {
    const x = mousePosition.elX;
    const y = mousePosition.elY;
    const { freezeRegionWidth, totalWidth, rowInitSize, columnInitSize, columnCount } =
      coordInstance;
    const rowIndex =
      y < 0 ? -Infinity : y <= rowInitSize ? -1 : coordInstance.getRowStartIndex(scrollTop + y);
    const columnIndex =
      x < 0
        ? -Infinity
        : scrollLeft + x > totalWidth && scrollLeft + x < totalWidth + columnAppendBtnWidth
          ? -2
          : x <= freezeRegionWidth
            ? x <= columnInitSize
              ? -1
              : coordInstance.getColumnStartIndex(x)
            : coordInstance.getColumnStartIndex(scrollLeft + x);

    return { x, y, rowIndex, columnIndex: Math.min(columnIndex, columnCount - 1) };
  };

  const getHoverCellPosition = (mouseState: IMouseState) => {
    const { rowIndex, columnIndex, x, y } = mouseState;
    const { realIndex, type } = getLinearRow(rowIndex);
    const isCellRange = columnIndex > -1 && type === LinearRowType.Row;

    if (isCellRange) {
      const cell = getCellContent([columnIndex, realIndex]);
      const cellRenderer = getCellRenderer(cell.type);

      if (
        cellRenderer.needsHoverPosition ||
        (cellRenderer.needsHoverPositionWhenActive &&
          activeCell &&
          isEqual(activeCell, [columnIndex, realIndex]))
      ) {
        const offsetX = coordInstance.getColumnOffset(columnIndex);
        return [
          columnIndex < freezeColumnCount ? x - offsetX : x - offsetX + scrollLeft,
          y - coordInstance.getRowOffset(rowIndex) + scrollTop,
        ] as ICellPosition;
      }
    }
    return null;
  };

  const { onAutoScroll, onAutoScrollStop } = useAutoScroll({
    coordInstance,
    isSelecting,
    isDragging,
    dragType,
    scrollBy,
  });

  const activeCellBound = useMemo(() => {
    if (_activeCellBound == null) return null;
    return {
      ..._activeCellBound,
      scrollTop: _activeCellBound.scrollEnable ? cellScrollTop : 0,
    };
  }, [_activeCellBound, cellScrollTop]);

  const getMouseState = () => {
    const position = getPosition();
    const { x, y } = position;
    const { totalHeight, totalWidth } = coordInstance;
    const isOutOfBounds =
      scrollLeft + x > totalWidth + columnAppendBtnWidth ||
      (scrollTop + y > totalHeight && !inRange(y, containerHeight, height));
    const regionData = getRegionData({
      position,
      dragState,
      selection,
      isSelecting,
      columnResizeState,
      columnStatistics,
      coordInstance,
      scrollState,
      rowControls,
      isFreezing,
      isOutOfBounds,
      isColumnResizable,
      isColumnAppendEnable,
      isMultiSelectionEnable,
      isColumnHeaderMenuVisible,
      isColumnFreezable,
      activeCellBound,
      activeCell,
      columns,
      height,
      theme,
      getLinearRow,
      real2RowIndex,
    });

    hoveredRegionRef.current = regionData;
    const { x: _x, y: _y, width: _w, height: _h, ...rest } = regionData;

    return {
      ...position,
      isOutOfBounds,
      ...rest,
    };
  };

  const setCursorStyle = (regionType: RegionType) => {
    if (isScrolling) return;
    if (isFreezing) return setCursor('grab');
    if (isDragging) return setCursor('grabbing');

    switch (regionType) {
      case RegionType.AppendRow: {
        if (activeCell != null) return;
        return setCursor('pointer');
      }
      case RegionType.AppendColumn:
      case RegionType.GroupStatistic:
      case RegionType.ColumnStatistic:
      case RegionType.ColumnHeaderMenu:
      case RegionType.ColumnDescription:
      case RegionType.ColumnPrimaryIcon:
      case RegionType.RowGroupHeader:
      case RegionType.RowHeaderExpandHandler:
        return setCursor('pointer');
      case RegionType.ColumnFreezeHandler:
        return setCursor('grab');
      case RegionType.AllCheckbox:
      case RegionType.RowHeaderCheckbox: {
        if (
          [SelectableType.None, SelectableType.Column, SelectableType.Cell].includes(
            selectable as SelectableType
          )
        ) {
          return setCursor('not-allowed');
        }
        return setCursor('pointer');
      }
      case RegionType.RowHeaderDragHandler: {
        if (draggable === DraggableType.Column || draggable === DraggableType.None) {
          return setCursor('not-allowed');
        }
        return setCursor('grabbing');
      }
      case RegionType.ColumnResizeHandler:
        return setCursor('ew-resize');
      case RegionType.FillHandler:
        return setCursor('crosshair');
      default:
        setCursor('default');
    }
  };

  // eslint-disable-next-line sonarjs/cognitive-complexity
  const onClick = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
    const mouseState = getMouseState();
    onSelectionClick(event, mouseState);
    const { type, rowIndex: hoverRowIndex, columnIndex } = mouseState;
    if (regionType !== type) return;

    const { realIndex: rowIndex } = getLinearRow(hoverRowIndex);

    switch (type) {
      case RegionType.AppendRow: {
        if (activeCell != null) {
          setSelection(selection.reset());
          return setActiveCell(null);
        }
        const linearRow = getLinearRow(hoverRowIndex - 1);
        onRowAppend?.(linearRow.realIndex);
        return;
      }
      case RegionType.AppendColumn:
        return onColumnAppend?.();
      case RegionType.RowHeaderExpandHandler:
        return onRowExpand?.(rowIndex);
      case RegionType.ColumnHeader:
        return onColumnHeaderClick?.(columnIndex, {
          x: coordInstance.getColumnRelativeOffset(columnIndex, scrollLeft),
          y: 0,
          width: coordInstance.getColumnWidth(columnIndex),
          height: columnHeadHeight,
        });
      case RegionType.ColumnHeaderMenu:
        return onColumnHeaderMenuClick?.(columnIndex, {
          x: coordInstance.getColumnRelativeOffset(columnIndex, scrollLeft),
          y: 0,
          width: coordInstance.getColumnWidth(columnIndex),
          height: columnHeadHeight,
        });
      case RegionType.GroupStatistic:
      case RegionType.ColumnStatistic: {
        const { x, y, width, height } = hoveredRegionRef.current;
        return onColumnStatisticClick?.(columnIndex, {
          x,
          y,
          width,
          height,
        });
      }
      case RegionType.Cell:
      case RegionType.ActiveCell: {
        const cell = getCellContent([columnIndex, rowIndex]) as IInnerCell;
        const cellRenderer = getCellRenderer(cell.type);
        const onCellClick = cellRenderer.onClick;
        const isActive =
          isEqual(prevActiveCellRef.current, activeCell) &&
          isEqual(activeCell, [columnIndex, rowIndex]);

        if (onCellClick && hoverCellPosition) {
          onCellClick(
            cell as never,
            {
              width: coordInstance.getColumnWidth(columnIndex),
              height: coordInstance.getRowHeight(hoverRowIndex),
              theme,
              hoverCellPosition,
              activeCellBound,
              isActive,
            },
            (cellRegion: ICellRegionWithData) => {
              const { type, data } = cellRegion;

              if (type === CellRegionType.Update) {
                return onCellEdited?.([columnIndex, rowIndex], {
                  ...cell,
                  data,
                } as IInnerCell);
              }

              if (type === CellRegionType.ToggleEditing) {
                return setEditing(true);
              }
            }
          );
        }
        return;
      }
      case RegionType.RowGroupHeader: {
        const { rowIndex } = mouseState;
        const linearRow = getLinearRow(rowIndex);
        if (linearRow.type !== LinearRowType.Group) return;
        const { id } = linearRow;

        if (collapsedGroupIds == null) {
          return onCollapsedGroupChanged?.(new Set([id]));
        }

        if (collapsedGroupIds.has(id)) {
          const newCollapsedGroupIds = new Set(collapsedGroupIds);
          newCollapsedGroupIds.delete(id);
          return onCollapsedGroupChanged?.(newCollapsedGroupIds);
        }
        return onCollapsedGroupChanged?.(new Set([...collapsedGroupIds, id]));
      }
    }

    const { type: clickRegionType, ...rest } = hoveredRegionRef.current;
    onItemClick?.(clickRegionType, rest, [columnIndex, rowIndex]);
  };

  const onDblClick = () => {
    const mouseState = getMouseState();
    const { type, rowIndex, columnIndex } = mouseState;
    const { realIndex } = getLinearRow(rowIndex);
    if (
      [RegionType.Cell, RegionType.ActiveCell].includes(type) &&
      isEqual(selectionRanges[0], [columnIndex, realIndex])
    ) {
      const cell = getCellContent([columnIndex, realIndex]) as IInnerCell;
      if (cell.readonly) return;
      editorContainerRef.current?.focus?.();
      return setEditing(true);
    }
    if (
      type === RegionType.ColumnHeader &&
      isEqual(selectionRanges[0], [columnIndex, columnIndex])
    ) {
      return onColumnHeaderDblClick?.(columnIndex, {
        x: coordInstance.getColumnRelativeOffset(columnIndex, scrollLeft),
        y: 0,
        width: coordInstance.getColumnWidth(columnIndex),
        height: columnHeadHeight,
      });
    }
  };

  const onSmartClick = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
    const eventDetail = event.detail;

    if (eventDetail === 1) {
      onClick(event);
    }

    if (eventDetail === 2) {
      onDblClick();
    }
  };

  const onMouseDown = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
    event.preventDefault();
    if (event.button === MouseButtonType.Right) return;
    const mouseState = getMouseState();
    setMouseState(mouseState);
    const { rowIndex: hoverRowIndex, columnIndex, type } = mouseState;
    const { realIndex: rowIndex } = getLinearRow(hoverRowIndex);
    if (
      !(
        isCellSelection &&
        isEqual(selectionRanges[0], [columnIndex, rowIndex]) &&
        type === RegionType.Cell
      )
    ) {
      setEditing(false);
      editorContainerRef.current?.saveValue?.();
    }
    onDragStart(mouseState, (type, ranges) => {
      if (type === DragRegionType.Columns) {
        _onDragStart?.(type, flatRanges(ranges));
      }
      if (type === DragRegionType.Rows) {
        const originRealIndexs = flatRanges(ranges).map((index) => getLinearRow(index).realIndex);
        _onDragStart?.(type, originRealIndexs);
      }
    });
    onColumnFreezeStart(mouseState);
    prevActiveCellRef.current = activeCell;
    onSelectionStart(event, mouseState);
    isColumnResizable && onColumnResizeStart(mouseState);
  };

  const onCellPosition = (mouseState: IMouseState) => {
    const { columnIndex, rowIndex, type } = mouseState;
    const { realIndex } = getLinearRow(rowIndex);
    const cell = getCellContent([columnIndex, realIndex]);
    const cellRenderer = getCellRenderer(cell.type);
    const { needsHover, needsHoverPosition, needsHoverWhenActive, needsHoverPositionWhenActive } =
      cellRenderer;
    const isActive = type === RegionType.ActiveCell;
    if ((needsHoverPosition || (needsHoverPositionWhenActive && isActive)) && hoverCellPosition) {
      const { type } =
        cellRenderer.checkRegion?.(cell as never, {
          width: coordInstance.getColumnWidth(columnIndex),
          height: coordInstance.getRowHeight(rowIndex),
          theme,
          isActive,
          activeCellBound,
          hoverCellPosition,
        }) ?? {};
      return type !== CellRegionType.Blank ? setCursor('pointer') : undefined;
    }
    if (needsHover || (needsHoverWhenActive && isActive)) {
      setCursor('pointer');
    }
  };

  const onMouseMove = () => {
    const mouseState = getMouseState();
    const hoverCellPosition = getHoverCellPosition(mouseState);
    setHoverCellPosition(() => hoverCellPosition);
    setMouseState(() => mouseState);
    setCursorStyle(mouseState.type);
    onCellPosition(mouseState);
    onAutoScroll(mouseState);
    onSelectionChange(mouseState);
    onColumnResizeChange(mouseState, (newWidth, columnIndex) => {
      onColumnResize?.(columns[columnIndex], newWidth, columnIndex);
    });
    onDragChange(mouseState);
    onColumnFreezeMove(mouseState);
    if (!isInteracting && !isEqual(hoveredRegionRef.current, previousHoveredRegionRef.current)) {
      const { type, ...rest } = hoveredRegionRef.current;
      const { columnIndex, rowIndex } = mouseState;
      onItemHovered?.(type, rest, [columnIndex, getLinearRow(rowIndex).realIndex]);
    }
    previousHoveredRegionRef.current = { ...hoveredRegionRef.current };
  };

  const onMouseUp = () => {
    const mouseState = getMouseState();
    setMouseState(mouseState);
    onAutoScrollStop();
    onDragEnd(mouseState, (ranges, dropIndex) => {
      if (dragType === DragRegionType.Columns) {
        onColumnOrdered?.(flatRanges(ranges), dropIndex);
      }
      if (dragType === DragRegionType.Rows) {
        const { type: prevType } = getLinearRow(dropIndex - 1);
        const { type, realIndex } = getLinearRow(dropIndex);
        const originRealIndexs = flatRanges(ranges).map((index) => getLinearRow(index).realIndex);

        if (
          (prevType === LinearRowType.Row && type === LinearRowType.Append) ||
          (prevType === LinearRowType.Group && type === LinearRowType.Row && realIndex !== 0)
        ) {
          return onRowOrdered?.(originRealIndexs, realIndex + 1);
        }

        onRowOrdered?.(originRealIndexs, realIndex);
      }
      setActiveCell(null);
      setSelection(selection.reset());
      setCursor('default');
    });
    onColumnFreezeEnd((columnCount: number) => {
      onColumnFreeze?.(columnCount);
      setMouseState(DEFAULT_MOUSE_STATE);
    });
    onSelectionEnd();
    onColumnResizeEnd();
  };

  const onMouseLeave = () => {
    if (isInteracting) return;
    const { type, ...rest } = BLANK_REGION_DATA;
    onItemHovered?.(type, rest, [-Infinity, -Infinity]);
    setMouseState(DEFAULT_MOUSE_STATE);
    setHoveredColumnResizeIndex(-1);
  };

  const onContextMenuInner = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
    if (event.cancelable) event.preventDefault();
    if (onContextMenu == null) return;
    const mouseState = getMouseState();
    onSelectionContextMenu(mouseState, (selection, position) => onContextMenu(selection, position));
  };

  const resetState = () => {
    setActiveCell(null);
    setDragState(DEFAULT_DRAG_STATE);
    setMouseState(DEFAULT_MOUSE_STATE);
    setSelection(selection.reset());
    setHoveredColumnResizeIndex(-1);
    setColumnResizeState(DEFAULT_COLUMN_RESIZE_STATE);
  };

  useEventListener('mousemove', onMouseMove, isInteracting ? window : stageRef.current, true);
  useEventListener('mouseup', onMouseUp, isInteracting ? window : stageRef.current, true);

  useClickAway(containerRef, () => {
    setEditing(false);
    editorContainerRef.current?.saveValue?.();
  });

  useLayoutEffect(() => {
    if (activeColumnIndex == null || activeRowIndex == null) return;
    cellScrollerRef.current?.reset();
  }, [activeColumnIndex, activeRowIndex]);

  return (
    <div
      ref={containerRef}
      style={{
        width,
        height,
        cursor,
      }}
    >
      <div
        ref={stageRef}
        className="size-full"
        onClick={onSmartClick}
        onMouseDown={onMouseDown}
        onMouseLeave={onMouseLeave}
        onContextMenu={onContextMenuInner}
      >
        <RenderLayer
          theme={theme}
          width={width}
          height={height}
          columns={columns}
          commentCountMap={commentCountMap}
          columnStatistics={columnStatistics}
          coordInstance={coordInstance}
          rowControls={rowControls}
          imageManager={imageManager}
          spriteManager={spriteManager}
          visibleRegion={visibleRegion}
          collaborators={collaborators}
          activeCellBound={activeCellBound}
          activeCell={activeCell}
          mouseState={mouseState}
          scrollState={scrollState}
          dragState={dragState}
          selection={selection}
          groupCollection={groupCollection}
          forceRenderFlag={forceRenderFlag}
          rowIndexVisible={rowIndexVisible}
          columnResizeState={columnResizeState}
          columnFreezeState={columnFreezeState}
          columnHeaderVisible={columnHeaderVisible}
          hoverCellPosition={hoverCellPosition}
          hoveredColumnResizeIndex={hoveredColumnResizeIndex}
          isRowAppendEnable={isRowAppendEnable}
          isColumnFreezable={isColumnFreezable}
          isColumnResizable={isColumnResizable}
          isColumnAppendEnable={isColumnAppendEnable}
          isColumnHeaderMenuVisible={isColumnHeaderMenuVisible}
          isEditing={isEditing}
          isSelecting={isSelecting}
          isInteracting={isInteracting}
          isMultiSelectionEnable={isMultiSelectionEnable}
          getCellContent={getCellContent}
          real2RowIndex={real2RowIndex}
          getLinearRow={getLinearRow}
        />
      </div>

      {activeCellBound?.scrollEnable && (
        <CellScroller
          ref={cellScrollerRef}
          style={{
            top: coordInstance.getRowOffset(activeCellBound.rowIndex) + 4,
            left:
              coordInstance.getColumnRelativeOffset(activeCellBound.columnIndex + 1, scrollLeft) -
              10,
          }}
          containerRef={containerRef}
          activeCellBound={activeCellBound}
          setCellScrollTop={setCellScrollTop}
          scrollEnable={regionType === RegionType.ActiveCell}
        />
      )}

      <EditorContainer
        ref={editorContainerRef}
        theme={theme}
        isEditing={isEditing}
        selection={selection}
        activeCell={activeCell}
        scrollState={scrollState}
        coordInstance={coordInstance}
        activeCellBound={activeCellBound}
        onCopy={onCopy}
        onPaste={onPaste}
        onUndo={onUndo}
        onRedo={onRedo}
        onDelete={onDelete}
        onChange={onCellEdited}
        onRowExpand={onRowExpand}
        setEditing={setEditing}
        setSelection={setSelection}
        setActiveCell={setActiveCell}
        getCellContent={getCellContent}
        real2RowIndex={real2RowIndex}
        scrollToItem={scrollToItem}
      />
    </div>
  );
};

export const InteractionLayer = forwardRef(InteractionLayerBase);