Skip to content

Commit 90c00ae

Browse files
committed
fix(Table): improve horizontal scroll handling for fixedRows and headerAffixedTop
1 parent f2c04f2 commit 90c00ae

File tree

6 files changed

+158
-174
lines changed

6 files changed

+158
-174
lines changed

packages/components/table/BaseTable.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import React, { forwardRef, RefAttributes, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
2-
import classNames from 'classnames';
3-
import { pick } from 'lodash-es';
41
import log from '@tdesign/common-js/log/index';
52
import { getIEVersion } from '@tdesign/common-js/utils/helper';
3+
import classNames from 'classnames';
4+
import { pick } from 'lodash-es';
5+
import React, { forwardRef, RefAttributes, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
66
import Affix, { type AffixRef } from '../affix';
77
import useDefaultProps from '../hooks/useDefaultProps';
88
import useElementLazyRender from '../hooks/useElementLazyRender';
@@ -80,7 +80,7 @@ const BaseTable = forwardRef<BaseTableRef, BaseTableProps>((originalProps, ref)
8080
showAffixHeader,
8181
showAffixFooter,
8282
showAffixPagination,
83-
onHorizontalScroll,
83+
onTableHorizontalScroll,
8484
setTableContentRef,
8585
updateAffixHeaderOrFooter,
8686
} = useAffix(props, { showElement });
@@ -184,7 +184,7 @@ const BaseTable = forwardRef<BaseTableRef, BaseTableProps>((originalProps, ref)
184184

185185
const onFixedChange = () => {
186186
const timer = setTimeout(() => {
187-
onHorizontalScroll();
187+
onTableHorizontalScroll();
188188
updateAffixHeaderOrFooter();
189189
clearTimeout(timer);
190190
}, 0);
@@ -244,7 +244,7 @@ const BaseTable = forwardRef<BaseTableRef, BaseTableProps>((originalProps, ref)
244244
updateColumnFixedShadow(target);
245245
}
246246
lastScrollY = top;
247-
onHorizontalScroll(target);
247+
onTableHorizontalScroll(target);
248248
emitScrollEvent(e);
249249
};
250250

packages/components/table/_example/fixed-column.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useRef, useState } from 'react';
2-
import { Table, Radio, Checkbox, Space, Tag, Link } from 'tdesign-react';
3-
import { ErrorCircleFilledIcon, CheckCircleFilledIcon, CloseCircleFilledIcon } from 'tdesign-icons-react';
2+
import { Button, Checkbox, Link, Radio, Space, Table, Tag } from 'tdesign-react';
3+
import { CheckCircleFilledIcon, CloseCircleFilledIcon, ErrorCircleFilledIcon } from 'tdesign-icons-react';
44

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

@@ -27,13 +27,13 @@ const statusNameListMap = {
2727
};
2828

2929
export default function TableFixedColumn() {
30+
const tableRef = useRef(null);
31+
3032
const [tableLayout, setTableLayout] = useState<TableProps['tableLayout']>('fixed');
3133
const [emptyData, setEmptyData] = useState(false);
3234
const [leftFixedColumn, setLeftFixedColumn] = useState(2);
3335
const [rightFixedColumn, setReftFixedColumn] = useState(1);
3436

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

104-
<div>
105+
<Space align="center">
105106
<Radio.Group
106107
value={tableLayout}
107108
variant="default-filled"
@@ -110,10 +111,13 @@ export default function TableFixedColumn() {
110111
<Radio.Button value="fixed">table-layout: fixed</Radio.Button>
111112
<Radio.Button value="auto">table-layout: auto</Radio.Button>
112113
</Radio.Group>
113-
<Checkbox value={emptyData} onChange={setEmptyData} style={{ marginLeft: '16px', verticalAlign: 'middle' }}>
114+
<Button onClick={scrollToCreateTime} variant="dashed">
115+
滚动到指定列
116+
</Button>
117+
<Checkbox value={emptyData} onChange={setEmptyData}>
114118
空数据
115119
</Checkbox>
116-
</div>
120+
</Space>
117121

118122
{table}
119123
</Space>

packages/components/table/_example/fixed-header-col.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ for (let i = 0; i < 20; i++) {
1919
index: i,
2020
applicant: ['贾明', '张三', '王芳'][i % 3],
2121
status: i % 3,
22-
channel: ['电子签署', '纸质签署', '纸质签署'][i % 3],
2322
detail: {
2423
2524
},
@@ -65,7 +64,6 @@ export default function TableFixedColumn() {
6564
);
6665
},
6766
},
68-
{ colKey: 'channel', title: '签署方式' },
6967
{ colKey: 'matters', title: '申请事项', width: '150', foot: '-' },
7068
{ colKey: 'detail.email', title: '邮箱地址' },
7169
{ colKey: 'createTime', title: '申请日期', width: '120', foot: '-' },

packages/components/table/hooks/useAffix.ts

Lines changed: 61 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { useState, useRef, useMemo, useEffect } from 'react';
2-
import { TdBaseTableProps } from '../type';
3-
import { AffixProps } from '../../affix';
1+
import { useEffect, useMemo, useRef, useState } from 'react';
42
import { off, on } from '../../_util/listener';
3+
import type { AffixProps } from '../../affix';
4+
import type { TdBaseTableProps } from '../type';
55

66
/**
77
* 1. 表头吸顶(普通表头吸顶 和 虚拟滚动表头吸顶)
@@ -11,6 +11,9 @@ import { off, on } from '../../_util/listener';
1111
*/
1212
export default function useAffix(props: TdBaseTableProps, { showElement }: { showElement: boolean }) {
1313
const tableContentRef = useRef<HTMLDivElement>(null);
14+
const lastTableScrollLeftRef = useRef<number>(0);
15+
const lastPageScrollLeftRef = useRef<number>(0);
16+
1417
// 吸顶表头
1518
const affixHeaderRef = useRef<HTMLDivElement>(null);
1619
// 吸底表尾
@@ -19,6 +22,7 @@ export default function useAffix(props: TdBaseTableProps, { showElement }: { sho
1922
const horizontalScrollbarRef = useRef<HTMLDivElement>(null);
2023
// 吸底分页器
2124
const paginationRef = useRef<HTMLDivElement>(null);
25+
2226
// 当表格完全滚动消失在视野时,需要隐藏吸顶表头
2327
const [showAffixHeader, setShowAffixHeader] = useState(true);
2428
// 当表格完全滚动消失在视野时,需要隐藏吸底尾部
@@ -32,23 +36,23 @@ export default function useAffix(props: TdBaseTableProps, { showElement }: { sho
3236
);
3337

3438
const isAffixed = useMemo(
35-
() => !!(props.headerAffixedTop || props.footerAffixedBottom || props.horizontalScrollAffixedBottom),
36-
[props.footerAffixedBottom, props.headerAffixedTop, props.horizontalScrollAffixedBottom],
39+
() =>
40+
!!(isVirtualScroll || props.headerAffixedTop || props.footerAffixedBottom || props.horizontalScrollAffixedBottom),
41+
[isVirtualScroll, props.footerAffixedBottom, props.headerAffixedTop, props.horizontalScrollAffixedBottom],
3742
);
3843

39-
let lastScrollLeft = 0;
40-
const onHorizontalScroll = (scrollElement?: HTMLElement) => {
41-
if (!isAffixed && !isVirtualScroll) return;
44+
const onTableHorizontalScroll = (scrollElement?: HTMLElement) => {
45+
if (!isAffixed) return;
4246
let target = scrollElement;
4347
if (!target && tableContentRef.current) {
44-
lastScrollLeft = 0;
48+
lastTableScrollLeftRef.current = 0;
4549
target = tableContentRef.current;
4650
}
4751
if (!target) return;
4852
const left = target.scrollLeft;
4953
// 如果 lastScrollLeft 等于 left,说明不是横向滚动,不需要更新横向滚动距离
50-
if (lastScrollLeft === left) return;
51-
lastScrollLeft = left;
54+
if (lastTableScrollLeftRef.current === left) return;
55+
lastTableScrollLeftRef.current = left;
5256
// 表格内容、吸顶表头、吸底表尾、吸底横向滚动更新
5357
const toUpdateScrollElement = [
5458
tableContentRef.current,
@@ -63,6 +67,28 @@ export default function useAffix(props: TdBaseTableProps, { showElement }: { sho
6367
}
6468
};
6569

70+
const onPageHorizontalScroll = () => {
71+
if (!isAffixed || !props.fixedRows?.length) return;
72+
73+
const pageScrollLeft = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0;
74+
if (lastPageScrollLeftRef.current === pageScrollLeft) return;
75+
lastPageScrollLeftRef.current = pageScrollLeft;
76+
77+
const toUpdateScrollElement = [affixHeaderRef.current, affixFooterRef.current, horizontalScrollbarRef.current];
78+
toUpdateScrollElement.forEach((element) => {
79+
if (element) {
80+
// 固定行会使用绝对定位,吸顶表头的位置计算需要考虑页面级别的滚动偏移
81+
if (props.fixedRows.length) {
82+
// eslint-disable-next-line no-param-reassign
83+
element.style.marginLeft = `-${pageScrollLeft}px`;
84+
} else if (element.style.marginLeft) {
85+
// eslint-disable-next-line no-param-reassign
86+
element.style.marginLeft = '';
87+
}
88+
}
89+
});
90+
};
91+
6692
// 吸底的元素(footer、横向滚动条、分页器)是否显示
6793
const isAffixedBottomElementShow = (elementRect: DOMRect, tableRect: DOMRect, headerHeight: number) =>
6894
tableRect.top + headerHeight < elementRect.top && elementRect.top > elementRect.height;
@@ -73,7 +99,7 @@ export default function useAffix(props: TdBaseTableProps, { showElement }: { sho
7399
};
74100

75101
const updateAffixHeaderOrFooter = () => {
76-
if (!isAffixed && !isVirtualScroll) return;
102+
if (!isAffixed) return;
77103
const pos = tableContentRef.current?.getBoundingClientRect();
78104
if (!pos) return;
79105
const headerRect = tableContentRef.current?.querySelector('thead')?.getBoundingClientRect();
@@ -105,24 +131,25 @@ export default function useAffix(props: TdBaseTableProps, { showElement }: { sho
105131
}
106132
};
107133

108-
const onDocumentScroll = () => {
134+
const onPageScroll = () => {
109135
updateAffixHeaderOrFooter();
136+
onPageHorizontalScroll();
110137
};
111138

112139
const onFootScroll = () => {
113-
onHorizontalScroll(affixFooterRef.current);
140+
onTableHorizontalScroll(affixFooterRef.current);
114141
};
115142

116143
const onHeaderScroll = () => {
117-
onHorizontalScroll(affixHeaderRef.current);
144+
onTableHorizontalScroll(affixHeaderRef.current);
118145
};
119146

120147
const horizontalScrollbarScroll = () => {
121-
onHorizontalScroll(horizontalScrollbarRef.current);
148+
onTableHorizontalScroll(horizontalScrollbarRef.current);
122149
};
123150

124151
const onTableContentScroll = () => {
125-
onHorizontalScroll(tableContentRef.current);
152+
onTableHorizontalScroll(tableContentRef.current);
126153
};
127154

128155
const onFootMouseEnter = () => {
@@ -173,7 +200,7 @@ export default function useAffix(props: TdBaseTableProps, { showElement }: { sho
173200
on(horizontalScrollbarRef.current, 'mouseleave', onScrollbarMouseLeave);
174201
}
175202

176-
if ((isAffixed || isVirtualScroll) && tableContentRef.current) {
203+
if (isAffixed && tableContentRef.current) {
177204
on(tableContentRef.current, 'mouseenter', onTableContentMouseEnter);
178205
on(tableContentRef.current, 'mouseleave', onTableContentMouseLeave);
179206
}
@@ -198,14 +225,14 @@ export default function useAffix(props: TdBaseTableProps, { showElement }: { sho
198225
}
199226
};
200227

201-
const addVerticalScrollListener = () => {
228+
const addPageScrollListener = () => {
202229
if (typeof document === 'undefined') return;
203230
if (!isAffixed && !props.paginationAffixedBottom) return;
204231
const timer = setTimeout(() => {
205232
if (isAffixed || props.paginationAffixedBottom) {
206-
on(document, 'scroll', onDocumentScroll);
233+
on(document, 'scroll', onPageScroll);
207234
} else {
208-
off(document, 'scroll', onDocumentScroll);
235+
off(document, 'scroll', onPageScroll);
209236
}
210237
clearTimeout(timer);
211238
});
@@ -214,26 +241,28 @@ export default function useAffix(props: TdBaseTableProps, { showElement }: { sho
214241
useEffect(() => {
215242
const timer = setTimeout(() => {
216243
addHorizontalScrollListeners();
217-
onHorizontalScroll();
244+
onTableHorizontalScroll();
218245
updateAffixHeaderOrFooter();
219246
clearTimeout(timer);
220247
});
221-
222-
return removeHorizontalScrollListeners;
248+
return () => {
249+
removeHorizontalScrollListeners();
250+
};
223251
// eslint-disable-next-line react-hooks/exhaustive-deps
224-
}, [affixHeaderRef, affixFooterRef, horizontalScrollbarRef, tableContentRef, showElement]);
252+
}, [affixHeaderRef, affixFooterRef, horizontalScrollbarRef, tableContentRef, showElement, props.fixedRows]);
225253

226254
useEffect(() => {
227-
addVerticalScrollListener();
255+
addPageScrollListener();
228256
return () => {
229-
off(document, 'scroll', onDocumentScroll);
257+
if (typeof document === 'undefined') return;
258+
off(document, 'scroll', onPageScroll);
230259
};
231260
// eslint-disable-next-line react-hooks/exhaustive-deps
232-
}, [isAffixed]);
261+
}, [isAffixed, props.fixedRows]);
233262

234263
useEffect(() => {
235264
addHorizontalScrollListeners();
236-
onHorizontalScroll();
265+
onTableHorizontalScroll();
237266
// eslint-disable-next-line react-hooks/exhaustive-deps
238267
}, [
239268
props.data,
@@ -242,11 +271,12 @@ export default function useAffix(props: TdBaseTableProps, { showElement }: { sho
242271
props.footerAffixedBottom,
243272
props.horizontalScrollAffixedBottom,
244273
props.lazyLoad,
274+
props.fixedRows,
245275
]);
246276

247277
const setTableContentRef = (tableContent: HTMLDivElement) => {
248278
tableContentRef.current = tableContent;
249-
addVerticalScrollListener();
279+
addPageScrollListener();
250280
};
251281

252282
return {
@@ -257,7 +287,7 @@ export default function useAffix(props: TdBaseTableProps, { showElement }: { sho
257287
affixFooterRef,
258288
horizontalScrollbarRef,
259289
paginationRef,
260-
onHorizontalScroll,
290+
onTableHorizontalScroll,
261291
setTableContentRef,
262292
updateAffixHeaderOrFooter,
263293
};

0 commit comments

Comments
 (0)