index.js 8.0 KB

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