From fe77030f652a67bc41ac629ceafffb5bca882c71 Mon Sep 17 00:00:00 2001 From: Rylan Date: Thu, 20 Nov 2025 05:44:11 +0800 Subject: [PATCH 1/4] fix(Table): `dragSort` failure when affix header is not mounted --- packages/components/table/BaseTable.tsx | 17 +++-- packages/components/table/TBody.tsx | 8 +-- .../components/table/hooks/useDragSort.ts | 11 +-- .../components/table/hooks/useRefCallback.ts | 72 +++++++++++++++++++ 4 files changed, 88 insertions(+), 20 deletions(-) create mode 100644 packages/components/table/hooks/useRefCallback.ts diff --git a/packages/components/table/BaseTable.tsx b/packages/components/table/BaseTable.tsx index e8a2dc8021..60fa50e76c 100644 --- a/packages/components/table/BaseTable.tsx +++ b/packages/components/table/BaseTable.tsx @@ -1,6 +1,7 @@ import React, { forwardRef, RefAttributes, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; import classNames from 'classnames'; import { pick } from 'lodash-es'; + import log from '@tdesign/common-js/log/index'; import { getIEVersion } from '@tdesign/common-js/utils/helper'; import Affix, { type AffixRef } from '../affix'; @@ -18,6 +19,7 @@ import useClassName from './hooks/useClassName'; import useColumnResize from './hooks/useColumnResize'; import useFixed from './hooks/useFixed'; import usePagination from './hooks/usePagination'; +import useRefCallback from './hooks/useRefCallback'; import useStyle, { formatCSSUnit } from './hooks/useStyle'; import useTableHeader from './hooks/useTableHeader'; import { getAffixProps } from './utils'; @@ -52,11 +54,12 @@ const BaseTable = forwardRef((originalProps, ref) const tableRef = useRef(null); const tableElmRef = useRef(null); const bottomContentRef = useRef(null); + const [tableFootHeight, setTableFootHeight] = useState(0); + const [dividerBottom, setDividerBottom] = useState(0); - const allTableClasses = useClassName(); const { classPrefix, virtualScrollClasses, tableLayoutClasses, tableBaseClass, tableColFixedClasses } = - allTableClasses; + useClassName(); // 表格基础样式类 const { tableClasses, sizeClassNames, tableContentStyles, tableElementStyles } = useStyle(props); const { isMultipleHeader, spansAndLeafNodes, thList } = useTableHeader({ columns: props.columns }); @@ -146,12 +149,13 @@ const BaseTable = forwardRef((originalProps, ref) { [tableBaseClass.fullHeight]: height }, ]); + const { onMount: onAffixHeaderMount } = useRefCallback(affixHeaderRef); + const showRightDivider = useMemo( () => props.bordered && isFixedHeader && ((isMultipleHeader && isWidthOverflow) || !isMultipleHeader), [isFixedHeader, isMultipleHeader, isWidthOverflow, props.bordered], ); - const [dividerBottom, setDividerBottom] = useState(0); useEffect(() => { if (!bordered) return; const bottomRect = bottomContentRef.current?.getBoundingClientRect(); @@ -255,7 +259,7 @@ const BaseTable = forwardRef((originalProps, ref) const scrollColumnIntoView = (colKey: string) => { if (!tableContentRef.current) return; const thDom = tableContentRef.current.querySelector(`th[data-colkey="${colKey}"]`); - const fixedThDom = tableContentRef.current.querySelectorAll('th.t-table__cell--fixed-left'); + const fixedThDom = tableContentRef.current.querySelectorAll(`th.${classPrefix}-table__cell--fixed-left`); let totalWidth = 0; for (let i = 0, len = fixedThDom.length; i < len; i++) { totalWidth += fixedThDom[i].getBoundingClientRect().width; @@ -272,6 +276,7 @@ const BaseTable = forwardRef((originalProps, ref) tableHtmlElement: tableElmRef.current, tableContentElement: tableContentRef.current, affixHeaderElement: affixHeaderRef.current, + onAffixHeaderMount, refreshTable, scrollToElement: virtualConfig.scrollToElement, scrollColumnIntoView, @@ -375,7 +380,7 @@ const BaseTable = forwardRef((originalProps, ref) }; const affixedHeader = Boolean((headerAffixedTop || virtualConfig.isVirtualScroll) && tableWidth.current) && (
((originalProps, ref) tableContentRef, tableWidth, isWidthOverflow, - allTableClasses, rowKey, scroll: props.scroll, cellEmptyContent: props.cellEmptyContent, @@ -537,7 +541,6 @@ const BaseTable = forwardRef((originalProps, ref) ), // eslint-disable-next-line [ - allTableClasses, tableBodyProps.ellipsisOverlayClassName, tableBodyProps.rowAndColFixedPosition, tableBodyProps.showColumnShadow, diff --git a/packages/components/table/TBody.tsx b/packages/components/table/TBody.tsx index cc2441392a..8b9e463a07 100644 --- a/packages/components/table/TBody.tsx +++ b/packages/components/table/TBody.tsx @@ -2,8 +2,9 @@ import React, { type CSSProperties, type MutableRefObject, type ReactNode, useMemo } from 'react'; import classNames from 'classnames'; import { camelCase, get, pick } from 'lodash-es'; + import { useLocaleReceiver } from '../locale/LocalReceiver'; -import { TableClassName } from './hooks/useClassName'; +import useClassName from './hooks/useClassName'; import useRowspanAndColspan from './hooks/useRowspanAndColspan'; import TR, { ROW_LISTENERS, TABLE_PROPS, type TrProps } from './TR'; @@ -25,7 +26,6 @@ export interface TableBodyProps extends BaseTableProps { isWidthOverflow?: boolean; virtualConfig: VirtualScrollConfig; pagination?: PaginationProps; - allTableClasses?: TableClassName; handleRowMounted?: (params: RowMountedParams) => void; } @@ -74,7 +74,7 @@ const trProperties = [ export default function TBody(props: TableBodyProps) { // 如果不是变量复用,没必要对每一个参数进行解构(解构过程需要单独的内存空间存储临时变量) - const { data, columns, rowKey, firstFullRow, lastFullRow, virtualConfig, allTableClasses } = props; + const { data, columns, rowKey, firstFullRow, lastFullRow, virtualConfig } = props; const { isVirtualScroll } = virtualConfig; const renderData = isVirtualScroll ? virtualConfig.visibleData : data; @@ -84,7 +84,7 @@ export default function TBody(props: TableBodyProps) { const { skipSpansMap } = useRowspanAndColspan(data, columns, rowKey, props.rowspanAndColspan); const isSkipSnapsMapNotFinish = Boolean(props.rowspanAndColspan && !skipSpansMap.size); - const { tableFullRowClasses, tableBaseClass } = allTableClasses; + const { tableFullRowClasses, tableBaseClass } = useClassName(); const tbodyClasses = useMemo(() => [tableBaseClass.body], [tableBaseClass.body]); const hasFullRowConfig = useMemo(() => firstFullRow || lastFullRow, [firstFullRow, lastFullRow]); diff --git a/packages/components/table/hooks/useDragSort.ts b/packages/components/table/hooks/useDragSort.ts index b535b4068f..d4aace8a07 100644 --- a/packages/components/table/hooks/useDragSort.ts +++ b/packages/components/table/hooks/useDragSort.ts @@ -215,16 +215,9 @@ export default function useDragSort( if (!primaryTableRef || !primaryTableRef.current) return; registerRowDragEvent(primaryTableRef.current?.tableElement); registerColDragEvent(primaryTableRef.current?.tableHtmlElement); - /** 待表头节点准备完成后 */ - const timer = setTimeout(() => { - if (primaryTableRef.current?.affixHeaderElement) { - registerColDragEvent(primaryTableRef.current.affixHeaderElement); - } - clearTimeout(timer); + primaryTableRef.current.onAffixHeaderMount((node) => { + registerColDragEvent(node); }); - return () => { - clearTimeout(timer); - }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [primaryTableRef, columns, dragSort, innerPagination]); diff --git a/packages/components/table/hooks/useRefCallback.ts b/packages/components/table/hooks/useRefCallback.ts new file mode 100644 index 0000000000..72112756e1 --- /dev/null +++ b/packages/components/table/hooks/useRefCallback.ts @@ -0,0 +1,72 @@ +import { useCallback, useRef } from 'react'; + +/** + * 用于在 ref 挂载时触发回调函数 + */ +export default function useRefCallback(ref?: React.MutableRefObject) { + const callbacks = useRef void>>([]); + const unmountCallbacks = useRef void>>([]); + + /** + * 主要的 ref 回调函数 + * 1. 作为 ref 使用:
+ * 2. 注册回调:onMount((node) => { ... }) + */ + const onMount = useCallback( + (nodeOrCallback: T | ((node: T) => void)) => { + // 如果传入的是函数,则注册回调 + if (typeof nodeOrCallback === 'function') { + callbacks.current.push(nodeOrCallback); + return; + } + + // 否则是 ref 挂载 + const node = nodeOrCallback as T; + const prevNode = ref?.current; + + // 更新 ref + if (ref) { + // eslint-disable-next-line no-param-reassign + ref.current = node; + } + + // 如果是新挂载(从 null 变为有值),触发所有挂载回调 + if (node && !prevNode) { + callbacks.current.forEach((callback) => { + callback(node); + }); + } + + // 如果是卸载(从有值变为 null),触发所有卸载回调 + if (!node && prevNode) { + unmountCallbacks.current.forEach((callback) => { + callback(); + }); + } + + return node; + }, + [], // eslint-disable-line react-hooks/exhaustive-deps + ); + + /** + * 注册卸载回调 + */ + const onUnmount = useCallback((callback: () => void) => { + unmountCallbacks.current.push(callback); + }, []); + + /** + * 手动清除回调 + */ + const clearCallbacks = useCallback(() => { + callbacks.current = []; + unmountCallbacks.current = []; + }, []); + + return { + onMount, + onUnmount, + clearCallbacks, + }; +} From 1a5936583825a1e3392e2d5fe40bd4cc257867f0 Mon Sep 17 00:00:00 2001 From: Rylan Date: Thu, 20 Nov 2025 05:52:53 +0800 Subject: [PATCH 2/4] chore: improve null check --- packages/components/table/BaseTable.tsx | 4 ++-- .../hooks/{useRefCallback.ts => useDomRef.ts} | 18 +++--------------- packages/components/table/hooks/useDragSort.ts | 12 ++++++------ 3 files changed, 11 insertions(+), 23 deletions(-) rename packages/components/table/hooks/{useRefCallback.ts => useDomRef.ts} (79%) diff --git a/packages/components/table/BaseTable.tsx b/packages/components/table/BaseTable.tsx index 60fa50e76c..0b7f79f317 100644 --- a/packages/components/table/BaseTable.tsx +++ b/packages/components/table/BaseTable.tsx @@ -19,7 +19,7 @@ import useClassName from './hooks/useClassName'; import useColumnResize from './hooks/useColumnResize'; import useFixed from './hooks/useFixed'; import usePagination from './hooks/usePagination'; -import useRefCallback from './hooks/useRefCallback'; +import useDomRef from './hooks/useDomRef'; import useStyle, { formatCSSUnit } from './hooks/useStyle'; import useTableHeader from './hooks/useTableHeader'; import { getAffixProps } from './utils'; @@ -149,7 +149,7 @@ const BaseTable = forwardRef((originalProps, ref) { [tableBaseClass.fullHeight]: height }, ]); - const { onMount: onAffixHeaderMount } = useRefCallback(affixHeaderRef); + const { onMount: onAffixHeaderMount } = useDomRef(affixHeaderRef); const showRightDivider = useMemo( () => props.bordered && isFixedHeader && ((isMultipleHeader && isWidthOverflow) || !isMultipleHeader), diff --git a/packages/components/table/hooks/useRefCallback.ts b/packages/components/table/hooks/useDomRef.ts similarity index 79% rename from packages/components/table/hooks/useRefCallback.ts rename to packages/components/table/hooks/useDomRef.ts index 72112756e1..e23c6e6e01 100644 --- a/packages/components/table/hooks/useRefCallback.ts +++ b/packages/components/table/hooks/useDomRef.ts @@ -1,17 +1,9 @@ import { useCallback, useRef } from 'react'; -/** - * 用于在 ref 挂载时触发回调函数 - */ -export default function useRefCallback(ref?: React.MutableRefObject) { +function useDomRef(ref?: React.MutableRefObject) { const callbacks = useRef void>>([]); const unmountCallbacks = useRef void>>([]); - /** - * 主要的 ref 回调函数 - * 1. 作为 ref 使用:
- * 2. 注册回调:onMount((node) => { ... }) - */ const onMount = useCallback( (nodeOrCallback: T | ((node: T) => void)) => { // 如果传入的是函数,则注册回调 @@ -49,16 +41,10 @@ export default function useRefCallback(ref? [], // eslint-disable-line react-hooks/exhaustive-deps ); - /** - * 注册卸载回调 - */ const onUnmount = useCallback((callback: () => void) => { unmountCallbacks.current.push(callback); }, []); - /** - * 手动清除回调 - */ const clearCallbacks = useCallback(() => { callbacks.current = []; unmountCallbacks.current = []; @@ -70,3 +56,5 @@ export default function useRefCallback(ref? clearCallbacks, }; } + +export default useDomRef; diff --git a/packages/components/table/hooks/useDragSort.ts b/packages/components/table/hooks/useDragSort.ts index d4aace8a07..98703e8e6f 100644 --- a/packages/components/table/hooks/useDragSort.ts +++ b/packages/components/table/hooks/useDragSort.ts @@ -212,14 +212,14 @@ export default function useDragSort( // 注册拖拽事件 useEffect(() => { - if (!primaryTableRef || !primaryTableRef.current) return; - registerRowDragEvent(primaryTableRef.current?.tableElement); - registerColDragEvent(primaryTableRef.current?.tableHtmlElement); - primaryTableRef.current.onAffixHeaderMount((node) => { - registerColDragEvent(node); + if (!primaryTableRef.current) return; + registerRowDragEvent(primaryTableRef.current.tableElement); + registerColDragEvent(primaryTableRef.current.tableHtmlElement); + primaryTableRef.current.onAffixHeaderMount((el) => { + registerColDragEvent(el); }); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [primaryTableRef, columns, dragSort, innerPagination]); + }, [columns, dragSort, innerPagination]); return { isRowDraggable, From 70a439491045e4bf6a9223b4ac25737c16ff9f4c Mon Sep 17 00:00:00 2001 From: Rylan Date: Thu, 20 Nov 2025 06:41:00 +0800 Subject: [PATCH 3/4] chore: rename and move dictionary --- .../{table/hooks/useDomRef.ts => hooks/useDomRefLifecycle.ts} | 4 ++-- packages/components/table/BaseTable.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename packages/components/{table/hooks/useDomRef.ts => hooks/useDomRefLifecycle.ts} (91%) diff --git a/packages/components/table/hooks/useDomRef.ts b/packages/components/hooks/useDomRefLifecycle.ts similarity index 91% rename from packages/components/table/hooks/useDomRef.ts rename to packages/components/hooks/useDomRefLifecycle.ts index e23c6e6e01..1a38a7a537 100644 --- a/packages/components/table/hooks/useDomRef.ts +++ b/packages/components/hooks/useDomRefLifecycle.ts @@ -1,6 +1,6 @@ import { useCallback, useRef } from 'react'; -function useDomRef(ref?: React.MutableRefObject) { +function useDomRefLifecycle(ref?: React.MutableRefObject) { const callbacks = useRef void>>([]); const unmountCallbacks = useRef void>>([]); @@ -57,4 +57,4 @@ function useDomRef(ref?: React.MutableRefOb }; } -export default useDomRef; +export default useDomRefLifecycle; diff --git a/packages/components/table/BaseTable.tsx b/packages/components/table/BaseTable.tsx index 0b7f79f317..e89e7ef35e 100644 --- a/packages/components/table/BaseTable.tsx +++ b/packages/components/table/BaseTable.tsx @@ -17,9 +17,9 @@ import { baseTableDefaultProps } from './defaultProps'; import useAffix from './hooks/useAffix'; import useClassName from './hooks/useClassName'; import useColumnResize from './hooks/useColumnResize'; +import useDomRefLifecycle from '../hooks/useDomRefLifecycle'; import useFixed from './hooks/useFixed'; import usePagination from './hooks/usePagination'; -import useDomRef from './hooks/useDomRef'; import useStyle, { formatCSSUnit } from './hooks/useStyle'; import useTableHeader from './hooks/useTableHeader'; import { getAffixProps } from './utils'; @@ -149,7 +149,7 @@ const BaseTable = forwardRef((originalProps, ref) { [tableBaseClass.fullHeight]: height }, ]); - const { onMount: onAffixHeaderMount } = useDomRef(affixHeaderRef); + const { onMount: onAffixHeaderMount } = useDomRefLifecycle(affixHeaderRef); const showRightDivider = useMemo( () => props.bordered && isFixedHeader && ((isMultipleHeader && isWidthOverflow) || !isMultipleHeader), From 0d3257dc1fe5f504403fc1ead7329312a598fc54 Mon Sep 17 00:00:00 2001 From: Rylan Date: Thu, 27 Nov 2025 17:11:33 +0800 Subject: [PATCH 4/4] chore: rename --- .../hooks/{useDomRefLifecycle.ts => useDomRefMount.ts} | 4 ++-- packages/components/table/BaseTable.tsx | 6 +++--- packages/components/table/hooks/useDragSort.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) rename packages/components/hooks/{useDomRefLifecycle.ts => useDomRefMount.ts} (91%) diff --git a/packages/components/hooks/useDomRefLifecycle.ts b/packages/components/hooks/useDomRefMount.ts similarity index 91% rename from packages/components/hooks/useDomRefLifecycle.ts rename to packages/components/hooks/useDomRefMount.ts index 1a38a7a537..38bf7a7a43 100644 --- a/packages/components/hooks/useDomRefLifecycle.ts +++ b/packages/components/hooks/useDomRefMount.ts @@ -1,6 +1,6 @@ import { useCallback, useRef } from 'react'; -function useDomRefLifecycle(ref?: React.MutableRefObject) { +function useDomRefMount(ref: React.MutableRefObject) { const callbacks = useRef void>>([]); const unmountCallbacks = useRef void>>([]); @@ -57,4 +57,4 @@ function useDomRefLifecycle(ref?: React.Mut }; } -export default useDomRefLifecycle; +export default useDomRefMount; diff --git a/packages/components/table/BaseTable.tsx b/packages/components/table/BaseTable.tsx index e89e7ef35e..8a2d04f572 100644 --- a/packages/components/table/BaseTable.tsx +++ b/packages/components/table/BaseTable.tsx @@ -6,6 +6,7 @@ import log from '@tdesign/common-js/log/index'; import { getIEVersion } from '@tdesign/common-js/utils/helper'; import Affix, { type AffixRef } from '../affix'; import useDefaultProps from '../hooks/useDefaultProps'; +import useDomRefMount from '../hooks/useDomRefMount'; import useElementLazyRender from '../hooks/useElementLazyRender'; import useVirtualScroll from '../hooks/useVirtualScroll'; import Loading from '../loading'; @@ -17,7 +18,6 @@ import { baseTableDefaultProps } from './defaultProps'; import useAffix from './hooks/useAffix'; import useClassName from './hooks/useClassName'; import useColumnResize from './hooks/useColumnResize'; -import useDomRefLifecycle from '../hooks/useDomRefLifecycle'; import useFixed from './hooks/useFixed'; import usePagination from './hooks/usePagination'; import useStyle, { formatCSSUnit } from './hooks/useStyle'; @@ -117,6 +117,8 @@ const BaseTable = forwardRef((originalProps, ref) footerBottomAffixRef, }); + const { onMount: onAffixHeaderMount } = useDomRefMount(affixHeaderRef); + const { dataSource, innerPagination, isPaginateData, renderPagination } = usePagination(props, tableContentRef); // 列宽拖拽逻辑 @@ -149,8 +151,6 @@ const BaseTable = forwardRef((originalProps, ref) { [tableBaseClass.fullHeight]: height }, ]); - const { onMount: onAffixHeaderMount } = useDomRefLifecycle(affixHeaderRef); - const showRightDivider = useMemo( () => props.bordered && isFixedHeader && ((isMultipleHeader && isWidthOverflow) || !isMultipleHeader), [isFixedHeader, isMultipleHeader, isWidthOverflow, props.bordered], diff --git a/packages/components/table/hooks/useDragSort.ts b/packages/components/table/hooks/useDragSort.ts index 98703e8e6f..8408a9c478 100644 --- a/packages/components/table/hooks/useDragSort.ts +++ b/packages/components/table/hooks/useDragSort.ts @@ -215,7 +215,7 @@ export default function useDragSort( if (!primaryTableRef.current) return; registerRowDragEvent(primaryTableRef.current.tableElement); registerColDragEvent(primaryTableRef.current.tableHtmlElement); - primaryTableRef.current.onAffixHeaderMount((el) => { + primaryTableRef.current.onAffixHeaderMount((el: HTMLDivElement) => { registerColDragEvent(el); }); // eslint-disable-next-line react-hooks/exhaustive-deps