Skip to content

Commit 477c914

Browse files
committed
fix(Affix): container
1 parent 96e6638 commit 477c914

File tree

7 files changed

+80
-63
lines changed

7 files changed

+80
-63
lines changed

packages/components/affix/Affix.tsx

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
21
import { isFunction } from 'lodash-es';
2+
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
33

4+
import { isWindow } from '../_util/dom';
45
import { getScrollContainer } from '../_util/scroll';
56
import useConfig from '../hooks/useConfig';
67
import useDefaultProps from '../hooks/useDefaultProps';
@@ -37,31 +38,46 @@ const Affix = forwardRef<AffixRef, AffixProps>((props, ref) => {
3738
// top = 节点到页面顶部的距离,包含 scroll 中的高度
3839
const {
3940
top: wrapToTop = 0,
41+
bottom: wrapToBottom = 0,
4042
width: wrapWidth = 0,
4143
height: wrapHeight = 0,
42-
} = affixWrapRef.current?.getBoundingClientRect() ?? { top: 0 };
44+
} = affixWrapRef.current?.getBoundingClientRect() ?? { top: 0, bottom: 0 };
4345

4446
// 容器到页面顶部的距离, windows 为0
4547
let containerToTop = 0;
46-
if (scrollContainer.current instanceof HTMLElement) {
47-
containerToTop = scrollContainer.current.getBoundingClientRect().top;
48+
let containerToBottom = 0;
49+
if (isWindow(scrollContainer.current)) {
50+
containerToBottom = scrollContainer.current.innerHeight;
51+
} else if (scrollContainer.current instanceof HTMLElement) {
52+
const rect = scrollContainer.current.getBoundingClientRect();
53+
containerToTop = rect.top;
54+
containerToBottom = rect.bottom;
4855
}
4956

5057
const calcTop = wrapToTop - containerToTop; // 节点顶部到 container 顶部的距离
51-
const containerHeight =
52-
scrollContainer.current?.[scrollContainer.current instanceof Window ? 'innerHeight' : 'clientHeight'] -
53-
wrapHeight;
54-
const calcBottom = containerToTop + containerHeight - (offsetBottom ?? 0); // 计算 bottom 相对应的 top 值
5558

5659
let fixedTop: number | false;
57-
if (calcTop <= offsetTop) {
58-
// top 的触发
59-
fixedTop = containerToTop + offsetTop;
60-
} else if (wrapToTop >= calcBottom) {
61-
// bottom 的触发
62-
fixedTop = calcBottom;
60+
if (props.offsetBottom !== undefined && props.offsetTop === undefined) {
61+
const bottomThreshold = containerToBottom - (offsetBottom ?? 0);
62+
if (wrapToBottom >= bottomThreshold) {
63+
fixedTop = bottomThreshold - wrapHeight;
64+
} else {
65+
fixedTop = false;
66+
}
6367
} else {
64-
fixedTop = false;
68+
const containerHeight =
69+
scrollContainer.current?.[isWindow(scrollContainer.current) ? 'innerHeight' : 'clientHeight'] -
70+
wrapHeight;
71+
const calcBottom = containerToTop + containerHeight - (offsetBottom ?? 0); // 计算 bottom 相对应的 top 值
72+
if (calcTop <= offsetTop) {
73+
// top 的触发
74+
fixedTop = containerToTop + offsetTop;
75+
} else if (wrapToTop >= calcBottom) {
76+
// bottom 的触发
77+
fixedTop = calcBottom;
78+
} else {
79+
fixedTop = false;
80+
}
6581
}
6682

6783
if (affixRef.current) {
@@ -106,7 +122,7 @@ const Affix = forwardRef<AffixRef, AffixProps>((props, ref) => {
106122
});
107123
}
108124
ticking.current = true;
109-
}, [classPrefix, offsetBottom, offsetTop, onFixedChange, zIndex]);
125+
}, [classPrefix, offsetBottom, offsetTop, zIndex, onFixedChange, props.offsetBottom, props.offsetTop]);
110126

111127
useImperativeHandle(ref, () => ({
112128
handleScroll,
@@ -120,7 +136,7 @@ const Affix = forwardRef<AffixRef, AffixProps>((props, ref) => {
120136
useEffect(() => {
121137
const checkContainerExist = () => {
122138
const el = getScrollContainer(container);
123-
const isReady = el instanceof Window || el instanceof HTMLElement;
139+
const isReady = isWindow(el) || el instanceof HTMLElement;
124140
setContainerReady(isReady);
125141
return isReady;
126142
};
@@ -138,15 +154,16 @@ const Affix = forwardRef<AffixRef, AffixProps>((props, ref) => {
138154
subtree: true,
139155
});
140156

141-
return () => observer.disconnect();
157+
return () => {
158+
observer.disconnect();
159+
};
142160
}, [container]);
143161

144162
useEffect(() => {
145163
if (!containerReady) return;
146164

147165
const newContainer = getScrollContainer(container);
148166
if (!newContainer) return; // 容器没准备好
149-
if (scrollContainer.current === newContainer) return; // 绑定到相同的容器
150167

151168
// 清理旧的监听器
152169
if (scrollContainer.current) {
@@ -159,11 +176,21 @@ const Affix = forwardRef<AffixRef, AffixProps>((props, ref) => {
159176
scrollContainer.current.addEventListener('scroll', handleScroll);
160177
window.addEventListener('resize', handleScroll);
161178

179+
// 当 container 不是 window 时,也需要监听 window 的 scroll 事件
180+
// 这样当整个页面滚动时,可以确保 affix 元素不会超出容器范围
181+
const isContainerNotWindow = !isWindow(scrollContainer.current);
182+
if (isContainerNotWindow) {
183+
window.addEventListener('scroll', handleScroll);
184+
}
185+
162186
return () => {
163187
if (scrollContainer.current) {
164188
scrollContainer.current.removeEventListener('scroll', handleScroll);
165189
}
166190
window.removeEventListener('resize', handleScroll);
191+
if (isContainerNotWindow) {
192+
window.removeEventListener('scroll', handleScroll);
193+
}
167194
};
168195
}, [container, containerReady, handleScroll]);
169196

packages/components/affix/__tests__/affix.test.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { render, describe, vi, mockTimeout } from '@test/utils';
2+
import { describe, mockTimeout, render, vi } from '@test/utils';
33
import Affix from '../index';
44

55
describe('Affix 组件测试', () => {
@@ -97,12 +97,18 @@ describe('Affix 组件测试', () => {
9797
expect(getByText('固钉').parentNode).not.toHaveClass('t-affix');
9898
expect(getByText('固钉').parentElement?.style.zIndex).toBe('');
9999

100-
// offsetBottom
101-
const isWindow = getByText('固钉').parentElement && window instanceof Window;
102-
const { clientHeight } = document.documentElement;
103100
const { innerHeight } = window;
104-
await mockScrollTo((isWindow ? innerHeight : clientHeight) - 40);
105-
await mockScrollTo(isWindow ? innerHeight : clientHeight);
101+
mockFn.mockImplementation(() => ({
102+
top: innerHeight - 10,
103+
bottom: innerHeight,
104+
left: 0,
105+
right: 0,
106+
height: 10,
107+
width: 0,
108+
x: 0,
109+
y: 0,
110+
toJSON: () => ({}),
111+
}));
106112
await mockTimeout(() => false, 200);
107113
expect(onFixedChangeMock).toHaveBeenCalledTimes(1);
108114

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import React, { useState } from 'react';
22
import { Affix, Button } from 'tdesign-react';
33

4+
import type { AffixProps } from 'tdesign-react';
5+
46
export default function BaseExample() {
5-
const [top, setTop] = useState(150);
7+
const [affixed, setAffixed] = useState(false);
68

7-
const handleClick = () => {
8-
setTop(top + 10);
9+
const handleFixedChange: AffixProps['onFixedChange'] = (affixed, { top }) => {
10+
console.log('top', top);
11+
setAffixed(affixed);
912
};
1013

1114
return (
12-
<Affix offsetTop={top} offsetBottom={10}>
13-
<Button onClick={handleClick}>固钉</Button>
15+
<Affix offsetTop={150} zIndex={2000} onFixedChange={handleFixedChange}>
16+
<Button theme={affixed ? 'success' : 'primary'}>Affixed: {`${affixed}`}</Button>
1417
</Affix>
1518
);
1619
}

packages/components/affix/_example/container.tsx

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,24 @@
1-
import React, { useState, useRef, useEffect } from 'react';
1+
import React, { useState } from 'react';
22
import { Affix, Button } from 'tdesign-react';
3-
import type { AffixProps } from 'tdesign-react';
43

54
export default function ContainerExample() {
65
const [container, setContainer] = useState(null);
7-
const [affixed, setAffixed] = useState(false);
8-
const affixRef = useRef(null);
9-
10-
const handleFixedChange: AffixProps['onFixedChange'] = (affixed, { top }) => {
11-
console.log('top', top);
12-
setAffixed(affixed);
13-
};
14-
15-
useEffect(() => {
16-
if (affixRef.current) {
17-
const { handleScroll } = affixRef.current;
18-
// 防止 affix 移动到容器外
19-
window.addEventListener('scroll', handleScroll);
20-
return () => window.removeEventListener('scroll', handleScroll);
21-
}
22-
}, []);
236

247
const backgroundStyle = {
258
height: '1500px',
26-
paddingTop: '700px',
279
backgroundColor: '#eee',
2810
backgroundImage:
2911
'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)',
3012
backgroundSize: '30px 30px',
3113
backgroundPosition: '0 0,15px 15px,15px 15px,0 0',
32-
};
14+
display: 'flex',
15+
flexDirection: 'column',
16+
justifyContent: 'space-between',
17+
} as React.CSSProperties;
3318

3419
return (
3520
<div
21+
ref={setContainer}
3622
style={{
3723
border: '1px solid var(--component-stroke)',
3824
borderRadius: '3px',
@@ -41,18 +27,13 @@ export default function ContainerExample() {
4127
overflowY: 'auto',
4228
overscrollBehavior: 'none',
4329
}}
44-
ref={setContainer}
4530
>
4631
<div style={backgroundStyle}>
47-
<Affix
48-
offsetTop={50}
49-
offsetBottom={50}
50-
container={container}
51-
zIndex={5}
52-
onFixedChange={handleFixedChange}
53-
ref={affixRef}
54-
>
55-
<Button>affixed: {`${affixed}`}</Button>
32+
<Affix zIndex={10} offsetTop={50} container={container}>
33+
<Button>Top</Button>
34+
</Affix>
35+
<Affix offsetBottom={50} container={container}>
36+
<Button>Bottom</Button>
5637
</Affix>
5738
</div>
5839
</div>

packages/components/affix/affix.en-US.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ style | Object | - | CSS(Cascading Style Sheets),Typescript: `React.CSSPropert
1010
children | TNode | - | Typescript: `string \| TNode`[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N
1111
container | String / Function | () => (() => window) | Typescript: `ScrollContainer`[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N
1212
content | TNode | - | Typescript: `string \| TNode`[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N
13-
offsetBottom | Number | 0 | When the distance from the bottom of the container reaches the specified distance, the trigger is fixed | N
13+
offsetBottom | Number | - | When the distance from the bottom of the container reaches the specified distance, the trigger is fixed | N
1414
offsetTop | Number | 0 | When the distance from the top of the container reaches the specified distance, the trigger is fixed | N
1515
zIndex | Number | - | \- | N
1616
onFixedChange | Function | | Typescript: `(affixed: boolean, context: { top: number }) => void`<br/> | N

packages/components/affix/affix.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ style | Object | - | 样式,TS 类型:`React.CSSProperties` | N
1010
children | TNode | - | 内容,同 content。TS 类型:`string \| TNode`[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N
1111
container | String / Function | () => (() => window) | 指定滚动的容器。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'body' 或 () => document.body。TS 类型:`ScrollContainer`[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N
1212
content | TNode | - | 内容。TS 类型:`string \| TNode`[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N
13-
offsetBottom | Number | 0 | 距离容器顶部达到指定距离后触发固定 | N
13+
offsetBottom | Number | - | 距离容器顶部达到指定距离后触发固定 | N
1414
offsetTop | Number | 0 | 距离容器底部达到指定距离后触发固定 | N
1515
zIndex | Number | - | 固钉定位层级,样式默认为 500 | N
1616
onFixedChange | Function | | TS 类型:`(affixed: boolean, context: { top: number }) => void`<br/>固定状态发生变化时触发 | N

packages/components/affix/defaultProps.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44

55
import { TdAffixProps } from './type';
66

7-
export const affixDefaultProps: TdAffixProps = { container: () => window, offsetBottom: 0, offsetTop: 0 };
7+
export const affixDefaultProps: TdAffixProps = { container: () => window, offsetTop: 0 };

0 commit comments

Comments
 (0)