/
Affix.tsx
146 lines (119 loc) · 4.06 KB
/
Affix.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import React, { useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import getOffset from 'dom-lib/getOffset';
import { Offset, RsRefForwardingComponent, WithAsProps } from '../@types/common';
import { mergeRefs, useClassNames, useElementResize, useEventListener, useMount } from '../utils';
export interface AffixProps extends WithAsProps {
/** Distance from top */
top?: number;
/** Callback after the state changes. */
onChange?: (fixed?: boolean) => void;
/** Specify the container. */
container?: HTMLElement | (() => HTMLElement);
}
/**
* Get the layout size and offset of the mount element
*/
function useOffset(mountRef: React.RefObject<HTMLDivElement>) {
const [offset, setOffset] = useState<Offset | null>(null);
const updateOffset = useCallback(() => {
// FIXME upgrade dom-lib
setOffset(getOffset(mountRef.current!));
}, [mountRef]);
// Update after the element size changes
useElementResize(() => mountRef.current, updateOffset);
// Initialize after the first render
useMount(updateOffset);
// Update after window size changes
useEventListener(window, 'resize', updateOffset, false);
return offset;
}
/**
* Get the layout size and offset of the container element
* @param container
*/
function useContainerOffset(container) {
const [offset, setOffset] = useState<Offset | null>(null);
useEffect(() => {
const node = typeof container === 'function' ? container() : container;
setOffset(node ? getOffset(node) : null);
}, [container]);
return offset;
}
/**
* Check whether the current element should be in a fixed state.
* @param offset
* @param containerOffset
* @param props
*/
function useFixed(offset: Offset | null, containerOffset: Offset | null, props: AffixProps) {
const { top, onChange } = props;
const [fixed, setFixed] = useState<boolean>(false);
const handleScroll = useCallback(() => {
if (!offset) {
return;
}
const scrollY = window.scrollY || window.pageYOffset;
// When the scroll distance exceeds the element's top value, it is fixed.
let nextFixed = scrollY - (Number(offset.top) - Number(top)) >= 0;
// If the current element is specified in the container,
// add to determine whether the current container is in the window range.
if (containerOffset) {
nextFixed =
nextFixed && scrollY < Number(containerOffset.top) + Number(containerOffset.height);
}
if (nextFixed !== fixed) {
setFixed(nextFixed);
onChange?.(nextFixed);
}
}, [fixed, offset, containerOffset, onChange, top]);
// Add scroll event to window
useEventListener(window, 'scroll', handleScroll, false);
return fixed;
}
const Affix: RsRefForwardingComponent<'div', AffixProps> = React.forwardRef(
(props: AffixProps, ref) => {
const {
as: Component = 'div',
classPrefix = 'affix',
className,
children,
container,
top = 0,
onChange,
...rest
} = props;
const mountRef = useRef(null);
const offset = useOffset(mountRef);
const containerOffset = useContainerOffset(container);
const fixed = useFixed(offset, containerOffset, { top, onChange });
const { withClassPrefix, merge } = useClassNames(classPrefix);
const classes = merge(className, {
[withClassPrefix()]: fixed
});
const placeholderStyles = fixed ? { width: offset?.width, height: offset?.height } : undefined;
const fixedStyles: React.CSSProperties = {
position: 'fixed',
top,
left: offset?.left,
width: offset?.width,
zIndex: 10
};
const affixStyles = fixed ? fixedStyles : undefined;
return (
<Component {...rest} ref={mergeRefs(mountRef, ref)}>
<div className={classes} style={affixStyles}>
{children}
</div>
{fixed && <div aria-hidden style={placeholderStyles}></div>}
</Component>
);
}
);
Affix.displayName = 'Affix';
Affix.propTypes = {
top: PropTypes.number,
onChange: PropTypes.func,
container: PropTypes.oneOfType([PropTypes.any, PropTypes.func])
};
export default Affix;