You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何解决Kendo React Grid空网格间拖拽失效问题?

问题分析与解决方案

核心错误点

  1. 事件绑定逻辑错误:你给空Grid提示元素绑定的onDrop={dragStart}和onDragOver={dragEnd}完全不符合拖拽事件的处理逻辑——dragStart是拖拽开始时触发的函数,dragEnd是拖拽结束时的函数,空Grid需要的是处理拖拽悬停和放置完成的逻辑,且onDragOver必须阻止默认行为才能触发onDrop。
  2. Grid引用冲突:两个Grid共用同一个grid ref,导致组件引用混乱,可能影响拖拽相关的上下文判断。
  3. 空Grid无数据项时的逻辑缺失:原reorder函数依赖目标Grid中存在的相对数据项来插入,但空Grid没有数据项,需要单独处理直接添加选中项的逻辑。

修正步骤

1. 拆分Grid引用

将两个Grid的ref分开,避免引用冲突:

const grid1Ref = React.useRef(null);
const grid2Ref = React.useRef(null);

然后分别给两个Grid设置ref属性:

<Grid
  ref={grid1Ref}
  // 其他属性不变
>
  {/* ... */}
</Grid>

<Grid
  ref={grid2Ref}
  // 其他属性不变
>
  {/* ... */}
</Grid>

2. 新增空Grid的放置处理函数

添加专门处理空Grid放置的逻辑,直接将选中项添加到目标Grid并从原Grid移除:

const handleDropOnEmptyGrid = (targetGrid: 'first' | 'second') => {
  let updatedGrid1 = [...gridData1];
  let updatedGrid2 = [...gridData2];

  // 从两个Grid中移除已选中的项
  updatedGrid1 = removeDataItems(selection, updatedGrid1);
  updatedGrid2 = removeDataItems(selection, updatedGrid2);

  // 将选中项添加到目标空Grid
  if (targetGrid === 'first') {
    updatedGrid1 = [...updatedGrid1, ...selection];
  } else {
    updatedGrid2 = [...updatedGrid2, ...selection];
  }

  setGridData1(updatedGrid1);
  setGridData2(updatedGrid2);
  setActiveItem(null);
};

3. 修正GridNoRecords的事件绑定

给空Grid的提示元素绑定正确的拖拽事件,注意onDragOver必须阻止默认行为:

// 第一个Grid的GridNoRecords
<GridNoRecords>
  <div
    style={{ height: 350, backgroundColor: 'grey' }}
    onDragOver={(e) => {
      e.preventDefault(); // 必须阻止默认行为才能触发onDrop
    }}
    onDrop={() => handleDropOnEmptyGrid('first')}
  >
    Drag items here
  </div>
</GridNoRecords>

// 第二个Grid的GridNoRecords
<GridNoRecords>
  <div
    style={{ height: 350, backgroundColor: 'grey' }}
    onDragOver={(e) => {
      e.preventDefault(); // 必须阻止默认行为才能触发onDrop
    }}
    onDrop={() => handleDropOnEmptyGrid('second')}
  >
    Drag items here
  </div>
</GridNoRecords>

4. 调整上下文引用(可选)

如果你的GridContext用于拖拽相关逻辑,需要将其改为分别提供两个Grid的ref,或者根据实际使用场景调整上下文内容,避免引用混乱。

修正后的完整App组件代码

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import {
  Grid,
  GridNoRecords,
  GridColumn as Column,
} from '@progress/kendo-react-grid';
import { DragAndDrop } from '@progress/kendo-react-common';

import { DraggableRow } from './draggable-row';
import { SelectionCell } from './selection-cell';
import { DragHint } from './drag-hint';

import products from './products.json';
import { Product } from './interfaces';

export const ReorderContext = React.createContext<{
  reorder: (dataItem: Product, direction: 'before' | 'after' | null) => void;
  dragStart: (dataItem: Product) => void;
  dragEnd: (dataItem?: Product) => void;
}>({ reorder: () => {}, dragStart: () => {}, dragEnd: () => {} });
export const SelectionContext = React.createContext<[Product[], any]>([
  [],
  () => {},
]);
export const IsSelectedContext = React.createContext<boolean>(false);
export const DragHintContext =
  React.createContext<React.RefObject<HTMLElement> | null>(null);
export const GridContext = React.createContext<React.RefObject<Grid> | null>(
  null
);

const App = () => {
  const grid1Ref = React.useRef(null);
  const grid2Ref = React.useRef(null);
  const hint = React.useRef(null);
  const [gridData1, setGridData1] = React.useState(products.slice(0, 0));
  const [gridData2, setGridData2] = React.useState(products.slice(31, 60));
  const [selection, setSelection] = React.useState<Product[]>([]);
  const [activeItem, setActiveItem] = React.useState<Product | null>(null);

  const reorder = (relativeDataItem: any, direction: 'before' | 'after') => {
    let firstGridData = gridData1.slice();
    let secondGridData = gridData2.slice();

    const insertToFirst = firstGridData.some(
      (i) => i.ProductID === relativeDataItem.ProductID
    );

    const insertToSecond = secondGridData.some(
      (i) => i.ProductID === relativeDataItem.ProductID
    );

    firstGridData = removeDataItems(selection, firstGridData);
    secondGridData = removeDataItems(selection, secondGridData);

    if (insertToFirst) {
      firstGridData = insertDataItems(
        selection,
        relativeDataItem,
        direction,
        firstGridData
      );
    }

    if (insertToSecond) {
      secondGridData = insertDataItems(
        selection,
        relativeDataItem,
        direction,
        secondGridData
      );
    }

    setGridData1(firstGridData);
    setGridData2(secondGridData);
    setActiveItem(null);
  };

  const insertDataItems = (
    itemsToInsert,
    relativeDataItem,
    direction,
    collection
  ) => {
    let reorderedData = collection.slice();
    let nextIndex = reorderedData.findIndex(
      (p) => p.ProductID === relativeDataItem.ProductID
    );

    reorderedData.splice(
      Math.max(nextIndex + (direction === 'before' ? 0 : 1), 0),
      0,
      ...itemsToInsert
    );

    return reorderedData;
  };

  const removeDataItems = (itemsToRemove, collection) => {
    if (
      collection.some((i) =>
        itemsToRemove.some((s) => s.ProductID === i.ProductID)
      )
    ) {
      const newData = collection.filter(
        (i) => !itemsToRemove.some((s) => s.ProductID === i.ProductID)
      );
      return newData;
    }
    return collection;
  };

  const dragStart = (dataItem: any) => {
    setActiveItem(dataItem);
  };

  const dragEnd = () => {
    setActiveItem(null);
  };

  const handleDropOnEmptyGrid = (targetGrid: 'first' | 'second') => {
    let updatedGrid1 = [...gridData1];
    let updatedGrid2 = [...gridData2];

    updatedGrid1 = removeDataItems(selection, updatedGrid1);
    updatedGrid2 = removeDataItems(selection, updatedGrid2);

    if (targetGrid === 'first') {
      updatedGrid1 = [...updatedGrid1, ...selection];
    } else {
      updatedGrid2 = [...updatedGrid2, ...selection];
    }

    setGridData1(updatedGrid1);
    setGridData2(updatedGrid2);
    setActiveItem(null);
  };

  return (
    <GridContext.Provider value={null}>
      <DragHintContext.Provider value={hint}>
        <ReorderContext.Provider value={{ reorder, dragStart, dragEnd }}>
          <SelectionContext.Provider value={[selection, setSelection]}>
            <DragAndDrop>
              <Grid
                ref={grid1Ref}
                style={{ height: '400px' }}
                data={gridData1}
                dataItemKey={'ProductID'}
                rowRender={(row, rowProps) => (
                  <DraggableRow elementProps={row.props} {...rowProps} />
                )}
              >
                <GridNoRecords>
                  <div
                    style={{ height: 350, backgroundColor: 'grey' }}
                    onDragOver={(e) => e.preventDefault()}
                    onDrop={() => handleDropOnEmptyGrid('first')}
                  >
                    Drag items here
                  </div>
                </GridNoRecords>
                <Column title="" width="80px" cell={SelectionCell} />
                <Column field="ProductID" title="ID" width="60px" />
                <Column field="ProductName" title="Name" width="200px" />
                <Column field="Category.CategoryName" title="CategoryName" />
                <Column field="UnitPrice" title="Price" width="80px" />
                <Column field="UnitsInStock" title="In stock" width="80px" />
              </Grid>
              <hr />
              <Grid
                ref={grid2Ref}
                style={{ height: '400px' }}
                data={gridData2}
                dataItemKey={'ProductID'}
                rowRender={(row, rowProps) => (
                  <DraggableRow elementProps={row.props} {...rowProps} />
                )}
              >
                <GridNoRecords>
                  <div
                    style={{ height: 350, backgroundColor: 'grey' }}
                    onDragOver={(e) => e.preventDefault()}
                    onDrop={() => handleDropOnEmptyGrid('second')}
                  >
                    Drag items here
                  </div>
                </GridNoRecords>
                <Column title="" width="80px" cell={SelectionCell} />
                <Column field="ProductID" title="ID" width="60px" />
                <Column field="ProductName" title="Name" width="200px" />
                <Column field="Category.CategoryName" title="CategoryName" />
                <Column field="UnitPrice" title="Price" width="80px" />
                <Column field="UnitsInStock" title="In stock" width="80px" />
              </Grid>
              <DragHint
                ref={hint}
                portal={grid1Ref}
                className="k-card"
                style={{ display: activeItem ? undefined : 'none' }}
              >
                {activeItem && activeItem.ProductName}
                {selection.length > 1 && (
                  <div
                    style={{
                      position: 'absolute',
                      pointerEvents: 'none',
                      bottom: 0,
                      right: 0,
                      background: 'red',
                      padding: 8,
                      width: 32,
                      color: 'white',
                      borderRadius: '50%',
                      transform: 'translate(50%, 50%)',
                    }}
                  >
                    +{selection.length - 1}
                  </div>
                )}
              </DragHint>
            </DragAndDrop>
          </SelectionContext.Provider>
        </ReorderContext.Provider>
      </DragHintContext.Provider>
    </GridContext.Provider>
  );
};

ReactDOM.render(<App />, document.querySelector('my-app'));

关键说明

  • onDragOver必须调用e.preventDefault():浏览器默认会阻止拖拽放置行为,只有阻止默认行为后,onDrop事件才会触发。
  • 空Grid的放置逻辑独立于原有reorder函数:因为空Grid没有可参考的数据项,直接将选中项追加到目标Grid即可。
  • 拆分Grid ref避免引用冲突:确保每个Grid的引用独立,不会影响拖拽相关的上下文判断。

内容的提问来源于stack exchange,提问作者Pathar Lamak

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 10:25:25