UNPKG

@helpscout/hsds-react

Version:

React component library for Help Scout's Design System

170 lines (139 loc) 5.74 kB
"use strict"; exports.__esModule = true; exports.default = useFancyAnimationScroller; exports.exponentialDecay = exponentialDecay; exports.bindRequestAnimationFrame = bindRequestAnimationFrame; var _react = require("react"); var _useMeasureNode = require("./useMeasureNode"); // very difficult to test with JSDom, some basic interaction is tested in ScrollableContainer /* istanbul ignore file */ function useFancyAnimationScroller(_ref) { var container = _ref.container, _ref$decayRates = _ref.decayRates, decayRates = _ref$decayRates === void 0 ? [0.01, 0.05] : _ref$decayRates, nodeToAnimateFinalHeight = _ref.nodeToAnimateFinalHeight, nodeToAnimateSelector = _ref.nodeToAnimateSelector, nodeThatScrollsSelector = _ref.nodeThatScrollsSelector, _ref$topReachedClassN = _ref.topReachedClassNames, topReachedClassNames = _ref$topReachedClassN === void 0 ? 'at-the-top' : _ref$topReachedClassN; var initialHeight = (0, _react.useRef)(0); var lastScrollPosition = (0, _react.useRef)(0); var decayUp = decayRates[0], decayDown = decayRates[1]; var containerNode = getElement(container); if (containerNode) { var nodeToAnimate = containerNode.querySelector(nodeToAnimateSelector); var resizeObserver = (0, _useMeasureNode.setupObserver)({ cb: function cb(_ref2) { var height = _ref2.height; if (lastScrollPosition.current === 0) { var _nodeToAnimate$classL; initialHeight.current = height; (_nodeToAnimate$classL = nodeToAnimate.classList).add.apply(_nodeToAnimate$classL, [].concat(topReachedClassNames)); } else if (initialHeight.current === 0) { initialHeight.current = height; } }, dimensions: { height: true } }); resizeObserver.observe(nodeToAnimate); } (0, _react.useEffect)(function () { /** * Browsers behave differently when "remembering" the scroll position * of elements, for example for some reason Chrome doesn't remember * on this component while firefox does. * Here we make it consistent by just making them all "forget". */ window.addEventListener('unload', restoreScroll); return function () { window.removeEventListener('unload', restoreScroll); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [containerNode]); function restoreScroll() { var scrollableNode = containerNode.querySelector(nodeThatScrollsSelector); scrollableNode.scrollTop = 0; } function handleScroll() { var scrollableNode = containerNode.querySelector(nodeThatScrollsSelector); var scrollableFullHeight = scrollableNode.scrollHeight; var scrollTop = scrollableNode.scrollTop; var direction = lastScrollPosition.current < scrollTop ? 'down' : 'up'; lastScrollPosition.current = scrollTop; var nodeToAnimateInitialHeight = initialHeight.current; var nodeToAnimate = containerNode.querySelector(nodeToAnimateSelector); var nodeToAnimateCurrentHeight = nodeToAnimate.getBoundingClientRect().height; if (direction === 'down') { if (nodeToAnimateCurrentHeight > nodeToAnimateFinalHeight) { var rate = exponentialDecay(decayDown, scrollableFullHeight)(scrollTop); var percentage = rate * 100 / scrollableFullHeight; var progress = percentage * (nodeToAnimateInitialHeight - nodeToAnimateFinalHeight) / 100; var newHeight = nodeToAnimateInitialHeight - progress; nodeToAnimate.style.height = newHeight + "px"; if (newHeight >= nodeToAnimateFinalHeight * 0.75) { var _nodeToAnimate$classL2; (_nodeToAnimate$classL2 = nodeToAnimate.classList).remove.apply(_nodeToAnimate$classL2, [].concat(topReachedClassNames)); } } } else if (direction === 'up') { if (nodeToAnimateCurrentHeight <= nodeToAnimateInitialHeight) { var _rate = exponentialDecay(decayUp, scrollableFullHeight)(scrollTop); var _percentage = 100 - _rate * 100 / scrollableFullHeight; var _progress = nodeToAnimateInitialHeight * (_percentage / 100); var _newHeight = _progress < nodeToAnimateFinalHeight ? nodeToAnimateFinalHeight : _progress; if (scrollTop !== 0) { nodeToAnimate.style.height = _newHeight + "px"; } else { nodeToAnimate.style.height = null; } if (_newHeight === nodeToAnimateInitialHeight) { var _nodeToAnimate$classL3; (_nodeToAnimate$classL3 = nodeToAnimate.classList).add.apply(_nodeToAnimate$classL3, [].concat(topReachedClassNames)); } } } } return [bindRequestAnimationFrame(handleScroll, true)]; } function getElement(someRef) { if (someRef instanceof HTMLElement) return someRef; return someRef && someRef.current; } /** * Calculates a number from a scale of exponential decay at a given rate. * @param {Number} rate The rate of decay, the larger the number the quickest the decay * @param {Number} upper The limit value * @returns Number */ function exponentialDecay(rate, upper) { return function (t) { return upper * (1 - Math.exp(-(rate * t))); }; } /** * From https://stackoverflow.com/a/44779316 * * @param {Function} fn Callback function * @param {Boolean|undefined} [throttle] Optionally throttle callback * @return {Function} Bound function */ function bindRequestAnimationFrame(fn, throttle) { var isRunning; var that; var args; var run = function run() { isRunning = false; fn.apply(that, args); }; return function () { that = this; args = arguments; if (isRunning && throttle) { return; } isRunning = true; requestAnimationFrame(run); }; }