diff --git a/packages/components/affix/Affix.tsx b/packages/components/affix/Affix.tsx index c0a1a2b049..b587287a14 100644 --- a/packages/components/affix/Affix.tsx +++ b/packages/components/affix/Affix.tsx @@ -1,11 +1,14 @@ -import React, { useEffect, forwardRef, useCallback, useImperativeHandle, useRef } from 'react'; import { isFunction } from 'lodash-es'; -import { StyledProps, ScrollContainerElement } from '../common'; -import { TdAffixProps } from './type'; +import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'; + +import { isWindow } from '../_util/dom'; +import { getScrollContainer } from '../_util/scroll'; import useConfig from '../hooks/useConfig'; -import { affixDefaultProps } from './defaultProps'; import useDefaultProps from '../hooks/useDefaultProps'; -import { getScrollContainer } from '../_util/scroll'; +import { affixDefaultProps } from './defaultProps'; + +import type { ScrollContainerElement, StyledProps } from '../common'; +import type { TdAffixProps } from './type'; export interface AffixProps extends TdAffixProps, StyledProps {} @@ -19,6 +22,8 @@ const Affix = forwardRef((props, ref) => { const { classPrefix } = useConfig(); + const [containerReady, setContainerReady] = useState(false); + const affixRef = useRef(null); const affixWrapRef = useRef(null); const placeholderEL = useRef(null); @@ -33,31 +38,46 @@ const Affix = forwardRef((props, ref) => { // top = 节点到页面顶部的距离,包含 scroll 中的高度 const { top: wrapToTop = 0, + bottom: wrapToBottom = 0, width: wrapWidth = 0, height: wrapHeight = 0, - } = affixWrapRef.current?.getBoundingClientRect() ?? { top: 0 }; + } = affixWrapRef.current?.getBoundingClientRect() ?? { top: 0, bottom: 0 }; // 容器到页面顶部的距离, windows 为0 let containerToTop = 0; - if (scrollContainer.current instanceof HTMLElement) { - containerToTop = scrollContainer.current.getBoundingClientRect().top; + let containerToBottom = 0; + if (isWindow(scrollContainer.current)) { + containerToBottom = scrollContainer.current.innerHeight; + } else if (scrollContainer.current instanceof HTMLElement) { + const rect = scrollContainer.current.getBoundingClientRect(); + containerToTop = rect.top; + containerToBottom = rect.bottom; } const calcTop = wrapToTop - containerToTop; // 节点顶部到 container 顶部的距离 - const containerHeight = - scrollContainer.current?.[scrollContainer.current instanceof Window ? 'innerHeight' : 'clientHeight'] - - wrapHeight; - const calcBottom = containerToTop + containerHeight - (offsetBottom ?? 0); // 计算 bottom 相对应的 top 值 let fixedTop: number | false; - if (calcTop <= offsetTop) { - // top 的触发 - fixedTop = containerToTop + offsetTop; - } else if (wrapToTop >= calcBottom) { - // bottom 的触发 - fixedTop = calcBottom; + if (props.offsetBottom !== undefined && props.offsetTop === undefined) { + const bottomThreshold = containerToBottom - (offsetBottom ?? 0); + if (wrapToBottom >= bottomThreshold) { + fixedTop = bottomThreshold - wrapHeight; + } else { + fixedTop = false; + } } else { - fixedTop = false; + const containerHeight = + scrollContainer.current?.[isWindow(scrollContainer.current) ? 'innerHeight' : 'clientHeight'] - + wrapHeight; + const calcBottom = containerToTop + containerHeight - (offsetBottom ?? 0); // 计算 bottom 相对应的 top 值 + if (calcTop <= offsetTop) { + // top 的触发 + fixedTop = containerToTop + offsetTop; + } else if (wrapToTop >= calcBottom) { + // bottom 的触发 + fixedTop = calcBottom; + } else { + fixedTop = false; + } } if (affixRef.current) { @@ -102,7 +122,7 @@ const Affix = forwardRef((props, ref) => { }); } ticking.current = true; - }, [classPrefix, offsetBottom, offsetTop, onFixedChange, zIndex]); + }, [classPrefix, offsetBottom, offsetTop, zIndex, onFixedChange, props.offsetBottom, props.offsetTop]); useImperativeHandle(ref, () => ({ handleScroll, @@ -114,18 +134,65 @@ const Affix = forwardRef((props, ref) => { }, []); useEffect(() => { - scrollContainer.current = getScrollContainer(container); + const checkContainerExist = () => { + const el = getScrollContainer(container); + const isReady = isWindow(el) || el instanceof HTMLElement; + setContainerReady(isReady); + return isReady; + }; + + if (checkContainerExist()) return; + + const observer = new MutationObserver(() => { + if (checkContainerExist()) { + observer.disconnect(); + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + }); + + return () => { + observer.disconnect(); + }; + }, [container]); + + useEffect(() => { + if (!containerReady) return; + + const newContainer = getScrollContainer(container); + if (!newContainer) return; // 容器没准备好 + + // 清理旧的监听器 if (scrollContainer.current) { - handleScroll(); - scrollContainer.current.addEventListener('scroll', handleScroll); - window.addEventListener('resize', handleScroll); + scrollContainer.current.removeEventListener('scroll', handleScroll); + } - return () => { - scrollContainer.current.removeEventListener('scroll', handleScroll); - window.removeEventListener('resize', handleScroll); - }; + scrollContainer.current = newContainer; + + handleScroll(); + scrollContainer.current.addEventListener('scroll', handleScroll); + window.addEventListener('resize', handleScroll); + + // 当 container 不是 window 时,也需要监听 window 的 scroll 事件 + // 这样当整个页面滚动时,可以确保 affix 元素不会超出容器范围 + const isContainerNotWindow = !isWindow(scrollContainer.current); + if (isContainerNotWindow) { + window.addEventListener('scroll', handleScroll); } - }, [container, handleScroll]); + + return () => { + if (scrollContainer.current) { + scrollContainer.current.removeEventListener('scroll', handleScroll); + } + window.removeEventListener('resize', handleScroll); + if (isContainerNotWindow) { + window.removeEventListener('scroll', handleScroll); + } + }; + }, [container, containerReady, handleScroll]); return (
diff --git a/packages/components/affix/__tests__/affix.test.tsx b/packages/components/affix/__tests__/affix.test.tsx index dbfced9398..9174bc8f82 100644 --- a/packages/components/affix/__tests__/affix.test.tsx +++ b/packages/components/affix/__tests__/affix.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, describe, vi, mockTimeout } from '@test/utils'; +import { describe, mockTimeout, render, vi } from '@test/utils'; import Affix from '../index'; describe('Affix 组件测试', () => { @@ -97,12 +97,18 @@ describe('Affix 组件测试', () => { expect(getByText('固钉').parentNode).not.toHaveClass('t-affix'); expect(getByText('固钉').parentElement?.style.zIndex).toBe(''); - // offsetBottom - const isWindow = getByText('固钉').parentElement && window instanceof Window; - const { clientHeight } = document.documentElement; const { innerHeight } = window; - await mockScrollTo((isWindow ? innerHeight : clientHeight) - 40); - await mockScrollTo(isWindow ? innerHeight : clientHeight); + mockFn.mockImplementation(() => ({ + top: innerHeight - 10, + bottom: innerHeight, + left: 0, + right: 0, + height: 10, + width: 0, + x: 0, + y: 0, + toJSON: () => ({}), + })); await mockTimeout(() => false, 200); expect(onFixedChangeMock).toHaveBeenCalledTimes(1); diff --git a/packages/components/affix/_example/base.tsx b/packages/components/affix/_example/base.tsx index 5794b8b055..061d733049 100644 --- a/packages/components/affix/_example/base.tsx +++ b/packages/components/affix/_example/base.tsx @@ -1,16 +1,19 @@ import React, { useState } from 'react'; import { Affix, Button } from 'tdesign-react'; +import type { AffixProps } from 'tdesign-react'; + export default function BaseExample() { - const [top, setTop] = useState(150); + const [affixed, setAffixed] = useState(false); - const handleClick = () => { - setTop(top + 10); + const handleFixedChange: AffixProps['onFixedChange'] = (affixed, { top }) => { + console.log('top', top); + setAffixed(affixed); }; return ( - - + + ); } diff --git a/packages/components/affix/_example/container.tsx b/packages/components/affix/_example/container.tsx index a080f5e226..1cf259bfed 100644 --- a/packages/components/affix/_example/container.tsx +++ b/packages/components/affix/_example/container.tsx @@ -1,38 +1,24 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState } from 'react'; import { Affix, Button } from 'tdesign-react'; -import type { AffixProps } from 'tdesign-react'; export default function ContainerExample() { const [container, setContainer] = useState(null); - const [affixed, setAffixed] = useState(false); - const affixRef = useRef(null); - - const handleFixedChange: AffixProps['onFixedChange'] = (affixed, { top }) => { - console.log('top', top); - setAffixed(affixed); - }; - - useEffect(() => { - if (affixRef.current) { - const { handleScroll } = affixRef.current; - // 防止 affix 移动到容器外 - window.addEventListener('scroll', handleScroll); - return () => window.removeEventListener('scroll', handleScroll); - } - }, []); const backgroundStyle = { height: '1500px', - paddingTop: '700px', backgroundColor: '#eee', backgroundImage: 'linear-gradient(45deg,#bbb 25%,transparent 0),linear-gradient(45deg,transparent 75%,#bbb 0),linear-gradient(45deg,#bbb 25%,transparent 0),linear-gradient(45deg,transparent 75%,#bbb 0)', backgroundSize: '30px 30px', backgroundPosition: '0 0,15px 15px,15px 15px,0 0', - }; + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + } as React.CSSProperties; return (
- - + + + + +
diff --git a/packages/components/affix/affix.en-US.md b/packages/components/affix/affix.en-US.md index 6fdcf79381..110381acff 100644 --- a/packages/components/affix/affix.en-US.md +++ b/packages/components/affix/affix.en-US.md @@ -10,7 +10,7 @@ style | Object | - | CSS(Cascading Style Sheets),Typescript: `React.CSSPropert children | TNode | - | Typescript: `string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N container | String / Function | () => (() => window) | Typescript: `ScrollContainer`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N content | TNode | - | Typescript: `string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N -offsetBottom | Number | 0 | When the distance from the bottom of the container reaches the specified distance, the trigger is fixed | N +offsetBottom | Number | - | When the distance from the bottom of the container reaches the specified distance, the trigger is fixed | N offsetTop | Number | 0 | When the distance from the top of the container reaches the specified distance, the trigger is fixed | N zIndex | Number | - | \- | N onFixedChange | Function | | Typescript: `(affixed: boolean, context: { top: number }) => void`
| N diff --git a/packages/components/affix/affix.md b/packages/components/affix/affix.md index a0fa6a4f62..dca969a380 100644 --- a/packages/components/affix/affix.md +++ b/packages/components/affix/affix.md @@ -10,7 +10,7 @@ style | Object | - | 样式,TS 类型:`React.CSSProperties` | N children | TNode | - | 内容,同 content。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N container | String / Function | () => (() => window) | 指定滚动的容器。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'body' 或 () => document.body。TS 类型:`ScrollContainer`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N content | TNode | - | 内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N -offsetBottom | Number | 0 | 距离容器顶部达到指定距离后触发固定 | N +offsetBottom | Number | - | 距离容器顶部达到指定距离后触发固定 | N offsetTop | Number | 0 | 距离容器底部达到指定距离后触发固定 | N zIndex | Number | - | 固钉定位层级,样式默认为 500 | N onFixedChange | Function | | TS 类型:`(affixed: boolean, context: { top: number }) => void`
固定状态发生变化时触发 | N diff --git a/packages/components/affix/defaultProps.ts b/packages/components/affix/defaultProps.ts index 254e262a4b..3bb6e3e422 100644 --- a/packages/components/affix/defaultProps.ts +++ b/packages/components/affix/defaultProps.ts @@ -4,4 +4,4 @@ import { TdAffixProps } from './type'; -export const affixDefaultProps: TdAffixProps = { container: () => window, offsetBottom: 0, offsetTop: 0 }; +export const affixDefaultProps: TdAffixProps = { container: () => window, offsetTop: 0 }; diff --git a/packages/components/table/BaseTable.tsx b/packages/components/table/BaseTable.tsx index e8a2dc8021..fa1c42ee69 100644 --- a/packages/components/table/BaseTable.tsx +++ b/packages/components/table/BaseTable.tsx @@ -49,6 +49,9 @@ const BaseTable = forwardRef((originalProps, ref) lazyLoad, pagination, } = props; + + const borderWidth = props.bordered ? 1 : 0; + const tableRef = useRef(null); const tableElmRef = useRef(null); const bottomContentRef = useRef(null); @@ -105,7 +108,6 @@ const BaseTable = forwardRef((originalProps, ref) updateColumnFixedShadow, getThWidthList, updateThWidthList, - addTableResizeObserver, updateTableAfterColumnResize, } = useFixed(props, finalColumns, { paginationAffixRef, @@ -156,9 +158,12 @@ const BaseTable = forwardRef((originalProps, ref) if (!bordered) return; const bottomRect = bottomContentRef.current?.getBoundingClientRect(); const paginationRect = paginationRef.current?.getBoundingClientRect(); - const bottom = (bottomRect?.height || 0) + (paginationRect?.height || 0); + let bottom = (bottomRect?.height || 0) + (paginationRect?.height || 0); + if (props.horizontalScrollAffixedBottom) { + bottom -= scrollbarWidth + borderWidth; + } setDividerBottom(bottom); - }, [bottomContentRef, paginationRef, bordered]); + }, [bottomContentRef, paginationRef, bordered, props.horizontalScrollAffixedBottom, scrollbarWidth, borderWidth]); useEffect(() => { setUseFixedTableElmRef(tableElmRef.current); @@ -294,12 +299,6 @@ const BaseTable = forwardRef((originalProps, ref) // eslint-disable-next-line react-hooks/exhaustive-deps }, [tableContentRef]); - useEffect( - () => addTableResizeObserver(tableRef.current), - // eslint-disable-next-line react-hooks/exhaustive-deps - [tableRef], - ); - const newData = isPaginateData ? dataSource : data; const renderColGroup = (isFixedHeader = true) => ( @@ -353,9 +352,6 @@ const BaseTable = forwardRef((originalProps, ref) props.size, ]; - // 多级表头左边线缺失 - const affixedLeftBorder = props.bordered ? 1 : 0; - // IE浏览器需要遮挡header吸顶滚动条,要减去getBoundingClientRect.height的滚动条高度4像素 const IEHeaderWrap = getIEVersion() <= 11 ? 4 : 0; const affixHeaderHeight = (affixHeaderRef.current?.getBoundingClientRect().height || 0) - IEHeaderWrap; @@ -376,7 +372,7 @@ const BaseTable = forwardRef((originalProps, ref) const affixedHeader = Boolean((headerAffixedTop || virtualConfig.isVirtualScroll) && tableWidth.current) && (
((originalProps, ref) >
((originalProps, ref) tableWidth, tableElmWidth, affixHeaderRef, - affixedLeftBorder, + borderWidth, tableElmClasses, tableElementStyles, columns, @@ -657,7 +653,7 @@ const BaseTable = forwardRef((originalProps, ref) tableElementStyles, tableElmWidth, affixFooterRef, - affixedLeftBorder, + borderWidth, bordered, isWidthOverflow, scrollbarWidth, diff --git a/packages/components/table/TBody.tsx b/packages/components/table/TBody.tsx index cc2441392a..118a339827 100644 --- a/packages/components/table/TBody.tsx +++ b/packages/components/table/TBody.tsx @@ -81,7 +81,7 @@ export default function TBody(props: TableBodyProps) { const [global, t] = useLocaleReceiver('table'); - const { skipSpansMap } = useRowspanAndColspan(data, columns, rowKey, props.rowspanAndColspan); + const { skipSpansMap } = useRowspanAndColspan(renderData, columns, rowKey, props.rowspanAndColspan); const isSkipSnapsMapNotFinish = Boolean(props.rowspanAndColspan && !skipSpansMap.size); const { tableFullRowClasses, tableBaseClass } = allTableClasses; diff --git a/packages/components/table/hooks/useAffix.ts b/packages/components/table/hooks/useAffix.ts index c467dbc42e..83e5ec7bed 100644 --- a/packages/components/table/hooks/useAffix.ts +++ b/packages/components/table/hooks/useAffix.ts @@ -1,7 +1,8 @@ -import { useState, useRef, useMemo, useEffect } from 'react'; -import { TdBaseTableProps } from '../type'; -import { AffixProps } from '../../affix'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { off, on } from '../../_util/listener'; +import { getScrollContainer } from '../../_util/scroll'; +import type { AffixProps } from '../../affix'; +import type { TdBaseTableProps } from '../type'; /** * 1. 表头吸顶(普通表头吸顶 和 虚拟滚动表头吸顶) @@ -11,6 +12,10 @@ import { off, on } from '../../_util/listener'; */ export default function useAffix(props: TdBaseTableProps, { showElement }: { showElement: boolean }) { const tableContentRef = useRef(null); + // 自定义滚动容器 + const scrollContainersRef = useRef([]); + // 用于记录上一次滚动位置,避免闭包问题 + const lastScrollLeftRef = useRef(0); // 吸顶表头 const affixHeaderRef = useRef(null); // 吸底表尾 @@ -36,36 +41,53 @@ export default function useAffix(props: TdBaseTableProps, { showElement }: { sho [props.footerAffixedBottom, props.headerAffixedTop, props.horizontalScrollAffixedBottom], ); - let lastScrollLeft = 0; - const onHorizontalScroll = (scrollElement?: HTMLElement) => { - if (!isAffixed && !isVirtualScroll) return; - let target = scrollElement; - if (!target && tableContentRef.current) { - lastScrollLeft = 0; - target = tableContentRef.current; - } - if (!target) return; - const left = target.scrollLeft; - // 如果 lastScrollLeft 等于 left,说明不是横向滚动,不需要更新横向滚动距离 - if (lastScrollLeft === left) return; - lastScrollLeft = left; - // 表格内容、吸顶表头、吸底表尾、吸底横向滚动更新 - const toUpdateScrollElement = [ - tableContentRef.current, - affixHeaderRef.current, - affixFooterRef.current, - horizontalScrollbarRef.current, - ]; - for (let i = 0, len = toUpdateScrollElement.length; i < len; i++) { - if (toUpdateScrollElement[i] && scrollElement !== toUpdateScrollElement[i]) { - toUpdateScrollElement[i].scrollLeft = left; + const onHorizontalScroll = useCallback( + (scrollElement?: HTMLElement) => { + if (!isAffixed && !isVirtualScroll) return; + let target = scrollElement; + if (!target && tableContentRef.current) { + lastScrollLeftRef.current = 0; + target = tableContentRef.current; } - } - }; + if (!target) return; + const left = target.scrollLeft; + // 如果 lastScrollLeft 等于 left,说明不是横向滚动,不需要更新横向滚动距离 + if (lastScrollLeftRef.current === left) return; + lastScrollLeftRef.current = left; + // 表格内容、吸顶表头、吸底表尾、吸底横向滚动更新 + const toUpdateScrollElement = [ + tableContentRef.current, + affixHeaderRef.current, + affixFooterRef.current, + horizontalScrollbarRef.current, + ]; + for (let i = 0, len = toUpdateScrollElement.length; i < len; i++) { + if (toUpdateScrollElement[i] && scrollElement !== toUpdateScrollElement[i]) { + toUpdateScrollElement[i].scrollLeft = left; + } + } + }, + [isAffixed, isVirtualScroll], + ); // 吸底的元素(footer、横向滚动条、分页器)是否显示 - const isAffixedBottomElementShow = (elementRect: DOMRect, tableRect: DOMRect, headerHeight: number) => - tableRect.top + headerHeight < elementRect.top && elementRect.top > elementRect.height; + const isAffixedBottomElementShow = useCallback( + (elementRect: DOMRect, tableRect: DOMRect, headerHeight: number, scrollContainer?: HTMLElement) => { + // 如果有自定义滚动容器,需要相对于容器计算 + if (scrollContainer) { + const containerRect = scrollContainer.getBoundingClientRect(); + const containerBottom = containerRect.bottom; + // 表格内容区域在容器可视区内 + const tableVisibleInContainer = tableRect.top + headerHeight < containerBottom; + // 表格底部超出容器底部(需要吸底滚动条) + const tableBottomBelowContainer = tableRect.bottom > containerBottom; + return tableVisibleInContainer && tableBottomBelowContainer; + } + // 默认相对于 viewport 计算 + return tableRect.top + headerHeight < elementRect.top && elementRect.top > elementRect.height; + }, + [], + ); const getOffsetTop = (props: boolean | Partial) => { if (typeof props === 'boolean') return 0; @@ -198,17 +220,44 @@ export default function useAffix(props: TdBaseTableProps, { showElement }: { sho } }; + const getAffixContainers = useCallback(() => { + const containers: HTMLElement[] = []; + const affixConfigs = [props.headerAffixedTop, props.footerAffixedBottom, props.horizontalScrollAffixedBottom]; + for (let i = 0; i < affixConfigs.length; i++) { + const config = affixConfigs[i]; + if (typeof config === 'object' && config?.container) { + const el = getScrollContainer(config.container); + if (el instanceof HTMLElement && !containers.includes(el)) { + containers.push(el); + } + } + } + return containers; + }, [props.headerAffixedTop, props.footerAffixedBottom, props.horizontalScrollAffixedBottom]); + const addVerticalScrollListener = () => { if (typeof document === 'undefined') return; if (!isAffixed && !props.paginationAffixedBottom) return; - const timer = setTimeout(() => { - if (isAffixed || props.paginationAffixedBottom) { - on(document, 'scroll', onDocumentScroll); - } else { - off(document, 'scroll', onDocumentScroll); + + if (isAffixed || props.paginationAffixedBottom) { + on(document, 'scroll', onDocumentScroll); + const containers = getAffixContainers(); + // 移除旧的监听器 + for (let i = 0; i < scrollContainersRef.current.length; i++) { + off(scrollContainersRef.current[i], 'scroll', onDocumentScroll); } - clearTimeout(timer); - }); + scrollContainersRef.current = containers; + for (let i = 0; i < containers.length; i++) { + on(containers[i], 'scroll', onDocumentScroll); + } + updateAffixHeaderOrFooter(); + } else { + off(document, 'scroll', onDocumentScroll); + for (let i = 0; i < scrollContainersRef.current.length; i++) { + off(scrollContainersRef.current[i], 'scroll', onDocumentScroll); + } + scrollContainersRef.current = []; + } }; useEffect(() => { @@ -227,6 +276,10 @@ export default function useAffix(props: TdBaseTableProps, { showElement }: { sho addVerticalScrollListener(); return () => { off(document, 'scroll', onDocumentScroll); + for (let i = 0; i < scrollContainersRef.current.length; i++) { + off(scrollContainersRef.current[i], 'scroll', onDocumentScroll); + } + scrollContainersRef.current = []; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [isAffixed]); diff --git a/packages/components/table/hooks/useFixed.ts b/packages/components/table/hooks/useFixed.ts index 6447eb1945..ff27892e55 100644 --- a/packages/components/table/hooks/useFixed.ts +++ b/packages/components/table/hooks/useFixed.ts @@ -5,10 +5,9 @@ import log from '@tdesign/common-js/log/index'; import { getScrollbarWidthWithCSS } from '@tdesign/common-js/utils/getScrollbarWidth'; import { getIEVersion } from '@tdesign/common-js/utils/helper'; import { off, on } from '../../_util/listener'; -import useDebounce from '../../hooks/useDebounce'; import useDeepEffect from '../../hooks/useDeepEffect'; import usePrevious from '../../hooks/usePrevious'; -import { isLessThanIE11OrNotHaveResizeObserver, resizeObserverElement } from '../utils'; +import { resizeObserverElement } from '../utils'; import type { AffixRef } from '../../affix'; import type { ClassName, Styles } from '../../common'; @@ -369,10 +368,11 @@ export default function useFixed( }; const updateTableWidth = () => { - const rect = tableContentRef.current?.getBoundingClientRect?.(); + const tRef = tableContentRef.current; + const rect = tRef?.getBoundingClientRect?.(); if (!rect) return; - // 存在纵向滚动条,且固定表头时,需去除滚动条宽度 - const reduceWidth = isFixedHeader ? scrollbarWidth : 0; + // 去除滚动条宽度 + const reduceWidth = isWidthOverflow ? scrollbarWidth : 0; tableWidth.current = rect.width - reduceWidth - (props.bordered ? 1 : 0); const elmRect = tableElmRef?.current?.getBoundingClientRect(); if (elmRect?.width) { @@ -488,20 +488,6 @@ export default function useFixed( // eslint-disable-next-line react-hooks/exhaustive-deps }, [isFixedColumn, columns, tableContentRef]); - // 使用防抖函数,避免频繁触发 - const updateFixedHeaderByUseDebounce = useDebounce(() => { - updateFixedHeader(); - }, 30); - - /** - * 通过监测表格大小变化,来调用 updateFixedHeader 修改状态 - */ - useEffect(() => { - if (tableContentRef.current) { - return resizeObserverElement(tableContentRef.current, updateFixedHeaderByUseDebounce); - } - }, [updateFixedHeaderByUseDebounce]); - useDeepEffect(updateFixedHeader, [maxHeight, data, columns, bordered, tableContentRef]); useDeepEffect(() => { @@ -540,25 +526,14 @@ export default function useFixed( } }; - const onResize = useDebounce(() => { - refreshTable(); - }, 30); - - function addTableResizeObserver(tableElement: HTMLDivElement) { - /** - * IE 11 以下使用 window resize;IE 11 以上使用 ResizeObserver - * 抽离相关判断为单独的方法 - */ - if (isLessThanIE11OrNotHaveResizeObserver()) return; - off(window, 'resize', onResize); - if (!tableElmWidth.current) return; - // 抽离 resize 为单独的方法,通过回调来执行操作 - return resizeObserverElement(tableElement, () => { - refreshTable(); - }); - } - useEffect(() => { + if (!tableContentRef.current) return; + // IE 11 以上使用 ResizeObserver + return resizeObserverElement(tableContentRef.current, refreshTable); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useDeepEffect(() => { const scrollWidth = getScrollbarWidthWithCSS(); setScrollbarWidth(scrollWidth); @@ -567,20 +542,35 @@ export default function useFixed( const hasResizeObserver = hasWindow && typeof window.ResizeObserver !== 'undefined'; updateTableWidth(); updateThWidthListHandler(); - // IE 11 以下使用 window resize;IE 11 以上使用 ResizeObserver + // IE 11 以下使用 window resize if ((isWatchResize && getIEVersion() < 11) || !hasResizeObserver) { - on(window, 'resize', onResize); + on(window, 'resize', refreshTable); } return () => { if ((isWatchResize && getIEVersion() < 11) || !hasResizeObserver) { if (typeof window !== 'undefined') { - off(window, 'resize', onResize); + off(window, 'resize', refreshTable); } } }; + }, [isFixedColumn, isFixedHeader, isWidthOverflow, scrollbarWidth, notNeedThWidthList, data]); + + useEffect(() => { + // 针对表格放在 Dialog 等有动画效果元素里的场景 + const tableContent = tableContentRef.current; + if (!tableContent) return; + const onAnimationEnd = (e: AnimationEvent) => { + const target = e.target as HTMLElement; + if (!target || !target.contains(tableContent)) return; + refreshTable(); + }; + on(document, 'animationend', onAnimationEnd, { capture: true }); + return () => { + off(document, 'animationend', onAnimationEnd, { capture: true }); + }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isFixedColumn]); + }, []); const updateTableAfterColumnResize = () => { updateFixedStatus(); @@ -608,7 +598,6 @@ export default function useFixed( setUseFixedTableElmRef, getThWidthList, updateThWidthList, - addTableResizeObserver, updateTableAfterColumnResize, }; } diff --git a/packages/components/table/hooks/useRowspanAndColspan.ts b/packages/components/table/hooks/useRowspanAndColspan.ts index b9ee13e1d9..db15e96750 100644 --- a/packages/components/table/hooks/useRowspanAndColspan.ts +++ b/packages/components/table/hooks/useRowspanAndColspan.ts @@ -1,7 +1,8 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { get } from 'lodash-es'; import log from '@tdesign/common-js/log/index'; -import { BaseTableCellParams, BaseTableCol, TableRowData, TableRowspanAndColspanFunc } from '../type'; +import useIsomorphicLayoutEffect from '../../hooks/useLayoutEffect'; +import type { BaseTableCellParams, BaseTableCol, TableRowData, TableRowspanAndColspanFunc } from '../type'; export interface SkipSpansValue { colspan?: number; @@ -80,7 +81,7 @@ export default function useRowspanAndColspan( return map; }; - useEffect(() => { + useIsomorphicLayoutEffect(() => { if (!rowspanAndColspan) return; skipSpansMap.clear(); const result = getSkipSpansMap(data, columns, rowspanAndColspan); diff --git a/test/snap/__snapshots__/csr.test.jsx.snap b/test/snap/__snapshots__/csr.test.jsx.snap index 79ee6ef014..9ab8846904 100644 --- a/test/snap/__snapshots__/csr.test.jsx.snap +++ b/test/snap/__snapshots__/csr.test.jsx.snap @@ -11,7 +11,8 @@ exports[`csr snapshot test > csr test packages/components/affix/_example/base.ts - 固钉 + Affixed: + false
@@ -25,7 +26,7 @@ exports[`csr snapshot test > csr test packages/components/affix/_example/contain style="border-radius: 3px; height: 400px; overflow-x: hidden; overflow-y: auto;" >
@@ -36,8 +37,21 @@ exports[`csr snapshot test > csr test packages/components/affix/_example/contain - affixed: - false + Top + + +
+
+
+
+
@@ -150477,9 +150491,9 @@ exports[`csr snapshot test > csr test packages/components/upload/_example/single
`; -exports[`ssr snapshot test > ssr test packages/components/affix/_example/base.tsx 1`] = `"
"`; +exports[`ssr snapshot test > ssr test packages/components/affix/_example/base.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test packages/components/affix/_example/container.tsx 1`] = `"
"`; +exports[`ssr snapshot test > ssr test packages/components/affix/_example/container.tsx 1`] = `"
"`; exports[`ssr snapshot test > ssr test packages/components/alert/_example/base.tsx 1`] = `"
这是一条成功的消息提示
这是一条普通的消息提示
这是一条警示消息
高危操作/出错信息提示
"`; diff --git a/test/snap/__snapshots__/ssr.test.jsx.snap b/test/snap/__snapshots__/ssr.test.jsx.snap index dce5e8cc0c..34b2d0e0ab 100644 --- a/test/snap/__snapshots__/ssr.test.jsx.snap +++ b/test/snap/__snapshots__/ssr.test.jsx.snap @@ -1,8 +1,8 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`ssr snapshot test > ssr test packages/components/affix/_example/base.tsx 1`] = `"
"`; +exports[`ssr snapshot test > ssr test packages/components/affix/_example/base.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test packages/components/affix/_example/container.tsx 1`] = `"
"`; +exports[`ssr snapshot test > ssr test packages/components/affix/_example/container.tsx 1`] = `"
"`; exports[`ssr snapshot test > ssr test packages/components/alert/_example/base.tsx 1`] = `"
这是一条成功的消息提示
这是一条普通的消息提示
这是一条警示消息
高危操作/出错信息提示
"`;