Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/components/table/BaseTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,15 @@ const BaseTable = forwardRef<BaseTableRef, BaseTableProps>((originalProps, ref)

// 1. 表头吸顶;2. 表尾吸底;3. 底部滚动条吸底;4. 分页器吸底
const {
affixOffset,
affixHeaderRef,
affixFooterRef,
horizontalScrollbarRef,
paginationRef,
showAffixHeader,
showAffixFooter,
showAffixPagination,
onHorizontalScroll,
onTableHorizontalScroll,
setTableContentRef,
updateAffixHeaderOrFooter,
} = useAffix(props, { showElement });
Expand Down Expand Up @@ -184,7 +185,7 @@ const BaseTable = forwardRef<BaseTableRef, BaseTableProps>((originalProps, ref)

const onFixedChange = () => {
const timer = setTimeout(() => {
onHorizontalScroll();
onTableHorizontalScroll();
updateAffixHeaderOrFooter();
clearTimeout(timer);
}, 0);
Expand Down Expand Up @@ -244,7 +245,7 @@ const BaseTable = forwardRef<BaseTableRef, BaseTableProps>((originalProps, ref)
updateColumnFixedShadow(target);
}
lastScrollY = top;
onHorizontalScroll(target);
onTableHorizontalScroll(target);
emitScrollEvent(e);
};

Expand Down Expand Up @@ -628,6 +629,7 @@ const BaseTable = forwardRef<BaseTableRef, BaseTableProps>((originalProps, ref)
[
// eslint-disable-next-line
...headUseMemoDependencies,
affixOffset,
showAffixHeader,
tableWidth,
tableElmWidth,
Expand All @@ -646,7 +648,7 @@ const BaseTable = forwardRef<BaseTableRef, BaseTableProps>((originalProps, ref)
renderAffixedFooter,
// eslint-disable-next-line react-hooks/exhaustive-deps
[
// eslint-disable-next-line react-hooks/exhaustive-deps
affixOffset,
showAffixFooter,
isFixedHeader,
rowAndColFixedPosition,
Expand Down
18 changes: 11 additions & 7 deletions packages/components/table/_example/fixed-column.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useRef, useState } from 'react';
import { Table, Radio, Checkbox, Space, Tag, Link } from 'tdesign-react';
import { ErrorCircleFilledIcon, CheckCircleFilledIcon, CloseCircleFilledIcon } from 'tdesign-icons-react';
import { Button, Checkbox, Link, Radio, Space, Table, Tag } from 'tdesign-react';
import { CheckCircleFilledIcon, CloseCircleFilledIcon, ErrorCircleFilledIcon } from 'tdesign-icons-react';

import type { TableProps } from 'tdesign-react';

Expand All @@ -27,13 +27,13 @@ const statusNameListMap = {
};

export default function TableFixedColumn() {
const tableRef = useRef(null);

const [tableLayout, setTableLayout] = useState<TableProps['tableLayout']>('fixed');
const [emptyData, setEmptyData] = useState(false);
const [leftFixedColumn, setLeftFixedColumn] = useState(2);
const [rightFixedColumn, setReftFixedColumn] = useState(1);

const tableRef = useRef(null);
// eslint-disable-next-line
const scrollToCreateTime = () => {
// 横向滚动到指定列,一般用于列数量较多的场景
tableRef.current.scrollColumnIntoView('createTime');
Expand All @@ -42,6 +42,7 @@ export default function TableFixedColumn() {
const table = (
<Table
ref={tableRef}
style={{ maxWidth: '800px' }}
bordered
rowKey="index"
data={emptyData ? [] : data}
Expand Down Expand Up @@ -101,7 +102,7 @@ export default function TableFixedColumn() {
<Radio.Button value={2}>右侧固定两列</Radio.Button>
</Radio.Group>

<div>
<Space align="center">
<Radio.Group
value={tableLayout}
variant="default-filled"
Expand All @@ -110,10 +111,13 @@ export default function TableFixedColumn() {
<Radio.Button value="fixed">table-layout: fixed</Radio.Button>
<Radio.Button value="auto">table-layout: auto</Radio.Button>
</Radio.Group>
<Checkbox value={emptyData} onChange={setEmptyData} style={{ marginLeft: '16px', verticalAlign: 'middle' }}>
<Button onClick={scrollToCreateTime} variant="dashed">
滚动到指定列
</Button>
<Checkbox value={emptyData} onChange={setEmptyData}>
空数据
</Checkbox>
</div>
</Space>

{table}
</Space>
Expand Down
2 changes: 0 additions & 2 deletions packages/components/table/_example/fixed-header-col.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ for (let i = 0; i < 20; i++) {
index: i,
applicant: ['贾明', '张三', '王芳'][i % 3],
status: i % 3,
channel: ['电子签署', '纸质签署', '纸质签署'][i % 3],
detail: {
email: ['[email protected]', '[email protected]', '[email protected]'][i % 3],
},
Expand Down Expand Up @@ -65,7 +64,6 @@ export default function TableFixedColumn() {
);
},
},
{ colKey: 'channel', title: '签署方式' },
{ colKey: 'matters', title: '申请事项', width: '150', foot: '-' },
{ colKey: 'detail.email', title: '邮箱地址' },
{ colKey: 'createTime', title: '申请日期', width: '120', foot: '-' },
Expand Down
128 changes: 92 additions & 36 deletions packages/components/table/hooks/useAffix.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { useState, useRef, useMemo, useEffect } from 'react';
import { TdBaseTableProps } from '../type';
import { AffixProps } from '../../affix';
import { useEffect, useMemo, useRef, useState } from 'react';
import { off, on } from '../../_util/listener';
import type { AffixProps } from '../../affix';
import type { TdBaseTableProps } from '../type';

type AffixOffset = { left: number; top: number };
const INITIAL_AFFIX_OFFSET: AffixOffset = { left: 0, top: 0 };

/**
* 1. 表头吸顶(普通表头吸顶 和 虚拟滚动表头吸顶)
Expand All @@ -11,6 +14,8 @@ import { off, on } from '../../_util/listener';
*/
export default function useAffix(props: TdBaseTableProps, { showElement }: { showElement: boolean }) {
const tableContentRef = useRef<HTMLDivElement>(null);
const lastTableScrollLeftRef = useRef<number>(0);

// 吸顶表头
const affixHeaderRef = useRef<HTMLDivElement>(null);
// 吸底表尾
Expand All @@ -19,6 +24,12 @@ export default function useAffix(props: TdBaseTableProps, { showElement }: { sho
const horizontalScrollbarRef = useRef<HTMLDivElement>(null);
// 吸底分页器
const paginationRef = useRef<HTMLDivElement>(null);

// 初始化渲染表格时,记录其位置,用于后续计算偏移量
const initialTableRectRef = useRef<AffixOffset | null>(INITIAL_AFFIX_OFFSET);

const [affixOffset, setAffixOffset] = useState<AffixOffset>(INITIAL_AFFIX_OFFSET);

// 当表格完全滚动消失在视野时,需要隐藏吸顶表头
const [showAffixHeader, setShowAffixHeader] = useState(true);
// 当表格完全滚动消失在视野时,需要隐藏吸底尾部
Expand All @@ -32,23 +43,35 @@ export default function useAffix(props: TdBaseTableProps, { showElement }: { sho
);

const isAffixed = useMemo(
() => !!(props.headerAffixedTop || props.footerAffixedBottom || props.horizontalScrollAffixedBottom),
[props.footerAffixedBottom, props.headerAffixedTop, props.horizontalScrollAffixedBottom],
() =>
!!(
isVirtualScroll ||
props.headerAffixedTop ||
props.footerAffixedBottom ||
props.horizontalScrollAffixedBottom ||
props.paginationAffixedBottom
),
[
isVirtualScroll,
props.footerAffixedBottom,
props.headerAffixedTop,
props.horizontalScrollAffixedBottom,
props.paginationAffixedBottom,
],
);

let lastScrollLeft = 0;
const onHorizontalScroll = (scrollElement?: HTMLElement) => {
if (!isAffixed && !isVirtualScroll) return;
const onTableHorizontalScroll = (scrollElement?: HTMLElement) => {
if (!isAffixed) return;
let target = scrollElement;
if (!target && tableContentRef.current) {
lastScrollLeft = 0;
lastTableScrollLeftRef.current = 0;
target = tableContentRef.current;
}
if (!target) return;
const left = target.scrollLeft;
// 如果 lastScrollLeft 等于 left,说明不是横向滚动,不需要更新横向滚动距离
if (lastScrollLeft === left) return;
lastScrollLeft = left;
if (lastTableScrollLeftRef.current === left) return;
lastTableScrollLeftRef.current = left;
// 表格内容、吸顶表头、吸底表尾、吸底横向滚动更新
const toUpdateScrollElement = [
tableContentRef.current,
Expand All @@ -63,6 +86,23 @@ export default function useAffix(props: TdBaseTableProps, { showElement }: { sho
}
};

const onPageHorizonScroll = () => {
if (!isAffixed || !tableContentRef.current) return;
const { left, top } = tableContentRef.current.getBoundingClientRect();

const leftOffset = left - initialTableRectRef.current.left;
const topOffset = top - initialTableRectRef.current.top;
setAffixOffset({ left: leftOffset, top: topOffset });

const toUpdateScrollElement = [affixHeaderRef.current, affixFooterRef.current];
for (let i = 0, len = toUpdateScrollElement.length; i < len; i++) {
if (toUpdateScrollElement[i]) {
// top 具体的偏移逻辑交个 Affix 组件的底层即可
toUpdateScrollElement[i].style.marginLeft = `${leftOffset}px`;
}
}
};

// 吸底的元素(footer、横向滚动条、分页器)是否显示
const isAffixedBottomElementShow = (elementRect: DOMRect, tableRect: DOMRect, headerHeight: number) =>
tableRect.top + headerHeight < elementRect.top && elementRect.top > elementRect.height;
Expand All @@ -73,7 +113,7 @@ export default function useAffix(props: TdBaseTableProps, { showElement }: { sho
};

const updateAffixHeaderOrFooter = () => {
if (!isAffixed && !isVirtualScroll) return;
if (!isAffixed) return;
const pos = tableContentRef.current?.getBoundingClientRect();
if (!pos) return;
const headerRect = tableContentRef.current?.querySelector('thead')?.getBoundingClientRect();
Expand Down Expand Up @@ -105,24 +145,25 @@ export default function useAffix(props: TdBaseTableProps, { showElement }: { sho
}
};

const onDocumentScroll = () => {
const onPageScroll = () => {
updateAffixHeaderOrFooter();
onPageHorizonScroll();
};

const onFootScroll = () => {
onHorizontalScroll(affixFooterRef.current);
onTableHorizontalScroll(affixFooterRef.current);
};

const onHeaderScroll = () => {
onHorizontalScroll(affixHeaderRef.current);
onTableHorizontalScroll(affixHeaderRef.current);
};

const horizontalScrollbarScroll = () => {
onHorizontalScroll(horizontalScrollbarRef.current);
onTableHorizontalScroll(horizontalScrollbarRef.current);
};

const onTableContentScroll = () => {
onHorizontalScroll(tableContentRef.current);
onTableHorizontalScroll(tableContentRef.current);
};

const onFootMouseEnter = () => {
Expand Down Expand Up @@ -157,7 +198,7 @@ export default function useAffix(props: TdBaseTableProps, { showElement }: { sho
off(tableContentRef.current, 'scroll', onTableContentScroll);
};

const addHorizontalScrollListeners = () => {
const addTableHorizontalScrollListeners = () => {
if (affixHeaderRef.current) {
on(affixHeaderRef.current, 'mouseenter', onHeaderMouseEnter);
on(affixHeaderRef.current, 'mouseleave', onHeaderMouseLeave);
Expand All @@ -173,13 +214,13 @@ export default function useAffix(props: TdBaseTableProps, { showElement }: { sho
on(horizontalScrollbarRef.current, 'mouseleave', onScrollbarMouseLeave);
}

if ((isAffixed || isVirtualScroll) && tableContentRef.current) {
if (isAffixed && tableContentRef.current) {
on(tableContentRef.current, 'mouseenter', onTableContentMouseEnter);
on(tableContentRef.current, 'mouseleave', onTableContentMouseLeave);
}
};

const removeHorizontalScrollListeners = () => {
const removeTableHorizontalScrollListeners = () => {
if (affixHeaderRef.current) {
off(affixHeaderRef.current, 'mouseenter', onHeaderMouseEnter);
off(affixHeaderRef.current, 'mouseleave', onHeaderMouseLeave);
Expand All @@ -198,42 +239,48 @@ export default function useAffix(props: TdBaseTableProps, { showElement }: { sho
}
};

const addVerticalScrollListener = () => {
if (typeof document === 'undefined') return;
if (!isAffixed && !props.paginationAffixedBottom) return;
const addPageScrollListener = () => {
const timer = setTimeout(() => {
if (isAffixed || props.paginationAffixedBottom) {
on(document, 'scroll', onDocumentScroll);
if (isAffixed) {
onPageScroll(); // initial sync
on(window, 'scroll', onPageScroll);
} else {
off(document, 'scroll', onDocumentScroll);
off(window, 'scroll', onPageScroll);
}
clearTimeout(timer);
});
};

const refreshTablePosition = () => {
if (!tableContentRef.current) return;
const { left, top } = tableContentRef.current.getBoundingClientRect();
initialTableRectRef.current = { left, top };
};

useEffect(() => {
const timer = setTimeout(() => {
addHorizontalScrollListeners();
onHorizontalScroll();
addTableHorizontalScrollListeners();
onTableHorizontalScroll();
updateAffixHeaderOrFooter();
clearTimeout(timer);
});

return removeHorizontalScrollListeners;
return () => {
removeTableHorizontalScrollListeners();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [affixHeaderRef, affixFooterRef, horizontalScrollbarRef, tableContentRef, showElement]);

useEffect(() => {
addVerticalScrollListener();
addPageScrollListener();
return () => {
off(document, 'scroll', onDocumentScroll);
off(window, 'scroll', onPageScroll);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAffixed]);

useEffect(() => {
addHorizontalScrollListeners();
onHorizontalScroll();
addTableHorizontalScrollListeners();
updateAffixHeaderOrFooter();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
props.data,
Expand All @@ -244,20 +291,29 @@ export default function useAffix(props: TdBaseTableProps, { showElement }: { sho
props.lazyLoad,
]);

useEffect(() => {
on(window, 'resize', refreshTablePosition);
return () => {
off(window, 'resize', refreshTablePosition);
};
}, []);

const setTableContentRef = (tableContent: HTMLDivElement) => {
tableContentRef.current = tableContent;
addVerticalScrollListener();
refreshTablePosition();
addPageScrollListener();
};

return {
affixOffset,
showAffixHeader,
showAffixFooter,
showAffixPagination,
affixHeaderRef,
affixFooterRef,
horizontalScrollbarRef,
paginationRef,
onHorizontalScroll,
onTableHorizontalScroll,
setTableContentRef,
updateAffixHeaderOrFooter,
};
Expand Down
Loading
Loading