UNPKG

7.87 kBJavaScriptView Raw
1"use client";
2
3import React from 'react';
4import classNames from 'classnames';
5import ResizeObserver from 'rc-resize-observer';
6import omit from "rc-util/es/omit";
7import throttleByAnimationFrame from '../_util/throttleByAnimationFrame';
8import { ConfigContext } from '../config-provider';
9import useStyle from './style';
10import { getFixedBottom, getFixedTop, getTargetRect } from './utils';
11const TRIGGER_EVENTS = ['resize', 'scroll', 'touchstart', 'touchmove', 'touchend', 'pageshow', 'load'];
12function getDefaultTarget() {
13 return typeof window !== 'undefined' ? window : null;
14}
15var AffixStatus;
16(function (AffixStatus) {
17 AffixStatus[AffixStatus["None"] = 0] = "None";
18 AffixStatus[AffixStatus["Prepare"] = 1] = "Prepare";
19})(AffixStatus || (AffixStatus = {}));
20const Affix = /*#__PURE__*/React.forwardRef((props, ref) => {
21 var _a;
22 const {
23 style,
24 offsetTop,
25 offsetBottom,
26 prefixCls,
27 className,
28 rootClassName,
29 children,
30 target,
31 onChange
32 } = props;
33 const {
34 getPrefixCls,
35 getTargetContainer
36 } = React.useContext(ConfigContext);
37 const affixPrefixCls = getPrefixCls('affix', prefixCls);
38 const [lastAffix, setLastAffix] = React.useState(false);
39 const [affixStyle, setAffixStyle] = React.useState();
40 const [placeholderStyle, setPlaceholderStyle] = React.useState();
41 const status = React.useRef(AffixStatus.None);
42 const prevTarget = React.useRef(null);
43 const prevListener = React.useRef();
44 const placeholderNodeRef = React.useRef(null);
45 const fixedNodeRef = React.useRef(null);
46 const timer = React.useRef(null);
47 const targetFunc = (_a = target !== null && target !== void 0 ? target : getTargetContainer) !== null && _a !== void 0 ? _a : getDefaultTarget;
48 const internalOffsetTop = offsetBottom === undefined && offsetTop === undefined ? 0 : offsetTop;
49 // =================== Measure ===================
50 const measure = () => {
51 if (status.current !== AffixStatus.Prepare || !fixedNodeRef.current || !placeholderNodeRef.current || !targetFunc) {
52 return;
53 }
54 const targetNode = targetFunc();
55 if (targetNode) {
56 const newState = {
57 status: AffixStatus.None
58 };
59 const placeholderRect = getTargetRect(placeholderNodeRef.current);
60 if (placeholderRect.top === 0 && placeholderRect.left === 0 && placeholderRect.width === 0 && placeholderRect.height === 0) {
61 return;
62 }
63 const targetRect = getTargetRect(targetNode);
64 const fixedTop = getFixedTop(placeholderRect, targetRect, internalOffsetTop);
65 const fixedBottom = getFixedBottom(placeholderRect, targetRect, offsetBottom);
66 if (fixedTop !== undefined) {
67 newState.affixStyle = {
68 position: 'fixed',
69 top: fixedTop,
70 width: placeholderRect.width,
71 height: placeholderRect.height
72 };
73 newState.placeholderStyle = {
74 width: placeholderRect.width,
75 height: placeholderRect.height
76 };
77 } else if (fixedBottom !== undefined) {
78 newState.affixStyle = {
79 position: 'fixed',
80 bottom: fixedBottom,
81 width: placeholderRect.width,
82 height: placeholderRect.height
83 };
84 newState.placeholderStyle = {
85 width: placeholderRect.width,
86 height: placeholderRect.height
87 };
88 }
89 newState.lastAffix = !!newState.affixStyle;
90 if (lastAffix !== newState.lastAffix) {
91 onChange === null || onChange === void 0 ? void 0 : onChange(newState.lastAffix);
92 }
93 status.current = newState.status;
94 setAffixStyle(newState.affixStyle);
95 setPlaceholderStyle(newState.placeholderStyle);
96 setLastAffix(newState.lastAffix);
97 }
98 };
99 const prepareMeasure = () => {
100 var _a;
101 status.current = AffixStatus.Prepare;
102 measure();
103 if (process.env.NODE_ENV === 'test') {
104 (_a = props === null || props === void 0 ? void 0 : props.onTestUpdatePosition) === null || _a === void 0 ? void 0 : _a.call(props);
105 }
106 };
107 const updatePosition = throttleByAnimationFrame(() => {
108 prepareMeasure();
109 });
110 const lazyUpdatePosition = throttleByAnimationFrame(() => {
111 // Check position change before measure to make Safari smooth
112 if (targetFunc && affixStyle) {
113 const targetNode = targetFunc();
114 if (targetNode && placeholderNodeRef.current) {
115 const targetRect = getTargetRect(targetNode);
116 const placeholderRect = getTargetRect(placeholderNodeRef.current);
117 const fixedTop = getFixedTop(placeholderRect, targetRect, internalOffsetTop);
118 const fixedBottom = getFixedBottom(placeholderRect, targetRect, offsetBottom);
119 if (fixedTop !== undefined && affixStyle.top === fixedTop || fixedBottom !== undefined && affixStyle.bottom === fixedBottom) {
120 return;
121 }
122 }
123 }
124 // Directly call prepare measure since it's already throttled.
125 prepareMeasure();
126 });
127 const addListeners = () => {
128 const listenerTarget = targetFunc === null || targetFunc === void 0 ? void 0 : targetFunc();
129 if (!listenerTarget) {
130 return;
131 }
132 TRIGGER_EVENTS.forEach(eventName => {
133 var _a;
134 if (prevListener.current) {
135 (_a = prevTarget.current) === null || _a === void 0 ? void 0 : _a.removeEventListener(eventName, prevListener.current);
136 }
137 listenerTarget === null || listenerTarget === void 0 ? void 0 : listenerTarget.addEventListener(eventName, lazyUpdatePosition);
138 });
139 prevTarget.current = listenerTarget;
140 prevListener.current = lazyUpdatePosition;
141 };
142 const removeListeners = () => {
143 if (timer.current) {
144 clearTimeout(timer.current);
145 timer.current = null;
146 }
147 const newTarget = targetFunc === null || targetFunc === void 0 ? void 0 : targetFunc();
148 TRIGGER_EVENTS.forEach(eventName => {
149 var _a;
150 newTarget === null || newTarget === void 0 ? void 0 : newTarget.removeEventListener(eventName, lazyUpdatePosition);
151 if (prevListener.current) {
152 (_a = prevTarget.current) === null || _a === void 0 ? void 0 : _a.removeEventListener(eventName, prevListener.current);
153 }
154 });
155 updatePosition.cancel();
156 lazyUpdatePosition.cancel();
157 };
158 React.useImperativeHandle(ref, () => ({
159 updatePosition
160 }));
161 // mount & unmount
162 React.useEffect(() => {
163 // [Legacy] Wait for parent component ref has its value.
164 // We should use target as directly element instead of function which makes element check hard.
165 timer.current = setTimeout(addListeners);
166 return () => removeListeners();
167 }, []);
168 React.useEffect(() => {
169 addListeners();
170 }, [target, affixStyle]);
171 React.useEffect(() => {
172 updatePosition();
173 }, [target, offsetTop, offsetBottom]);
174 const [wrapCSSVar, hashId, cssVarCls] = useStyle(affixPrefixCls);
175 const rootCls = classNames(rootClassName, hashId, affixPrefixCls, cssVarCls);
176 const mergedCls = classNames({
177 [rootCls]: affixStyle
178 });
179 let otherProps = omit(props, ['prefixCls', 'offsetTop', 'offsetBottom', 'target', 'onChange', 'rootClassName']);
180 if (process.env.NODE_ENV === 'test') {
181 otherProps = omit(otherProps, ['onTestUpdatePosition']);
182 }
183 return wrapCSSVar( /*#__PURE__*/React.createElement(ResizeObserver, {
184 onResize: updatePosition
185 }, /*#__PURE__*/React.createElement("div", Object.assign({
186 style: style,
187 className: className,
188 ref: placeholderNodeRef
189 }, otherProps), affixStyle && /*#__PURE__*/React.createElement("div", {
190 style: placeholderStyle,
191 "aria-hidden": "true"
192 }), /*#__PURE__*/React.createElement("div", {
193 className: mergedCls,
194 ref: fixedNodeRef,
195 style: affixStyle
196 }, /*#__PURE__*/React.createElement(ResizeObserver, {
197 onResize: updatePosition
198 }, children)))));
199});
200if (process.env.NODE_ENV !== 'production') {
201 Affix.displayName = 'Affix';
202}
203export default Affix;
\No newline at end of file