UNPKG

instantsearch-ui-components

Version:

Common UI components for InstantSearch.

463 lines (460 loc) 21 kB
import { _ as _$1 } from '@swc/helpers/esm/_instanceof.js'; import { _ as _$2 } from '@swc/helpers/esm/_object_spread.js'; import { _ } from '@swc/helpers/esm/_sliced_to_array.js'; import { _ as _$3 } from '@swc/helpers/esm/_type_of.js'; var DEFAULT_SPRING_ANIMATION = { /** * A value from 0 to 1, on how much to damp the animation. * 0 means no damping, 1 means full damping. * * @default 0.7 */ damping: 0.7, /** * The stiffness of how fast/slow the animation gets up to speed. * * @default 0.05 */ stiffness: 0.05, /** * The inertial mass associated with the animation. * Higher numbers make the animation slower. * * @default 1.25 */ mass: 1.25 }; var STICK_TO_BOTTOM_OFFSET_PX = 70; var SIXTY_FPS_INTERVAL_MS = 1000 / 60; var RETAIN_ANIMATION_DURATION_MS = 350; var mouseDown = false; if (typeof window !== 'undefined') { var _window_document, _window_document1, _window_document2; (_window_document = window.document) === null || _window_document === void 0 ? void 0 : _window_document.addEventListener('mousedown', function() { mouseDown = true; }); (_window_document1 = window.document) === null || _window_document1 === void 0 ? void 0 : _window_document1.addEventListener('mouseup', function() { mouseDown = false; }); (_window_document2 = window.document) === null || _window_document2 === void 0 ? void 0 : _window_document2.addEventListener('click', function() { mouseDown = false; }); } function createStickToBottom(param) { var useCallback = param.useCallback, useEffect = param.useEffect, useMemo = param.useMemo, useRef = param.useRef, useState = param.useState; return function useStickToBottom() { var options = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {}; var _useState = _(useState(false), 2), escapedFromLock = _useState[0], updateEscapedFromLock = _useState[1]; var _useState1 = _(useState(options.initial !== false), 2), isAtBottom = _useState1[0], updateIsAtBottom = _useState1[1]; var _useState2 = _(useState(false), 2), isNearBottom = _useState2[0], setIsNearBottom = _useState2[1]; var optionsRef = useRef(null); optionsRef.current = options; // Create refs early so they can be used in other hooks var scrollRef = useRef(null); var contentRef = useRef(null); var isSelecting = useCallback(function() { var _scrollRef_current; if (!mouseDown) { return false; } if (typeof window === 'undefined') { return false; } var selection = window.getSelection(); if (!selection || !selection.rangeCount) { return false; } var range = selection.getRangeAt(0); return range.commonAncestorContainer.contains(scrollRef.current) || ((_scrollRef_current = scrollRef.current) === null || _scrollRef_current === void 0 ? void 0 : _scrollRef_current.contains(range.commonAncestorContainer)); }, []); // biome-ignore lint/correctness/useExhaustiveDependencies: state is intentionally stable var state = useMemo(function() { var lastCalculation; return { escapedFromLock: escapedFromLock, isAtBottom: isAtBottom, resizeDifference: 0, accumulated: 0, velocity: 0, get scrollTop () { var _ref; var _scrollRef_current; return (_ref = (_scrollRef_current = scrollRef.current) === null || _scrollRef_current === void 0 ? void 0 : _scrollRef_current.scrollTop) !== null && _ref !== void 0 ? _ref : 0; }, set scrollTop (scrollTop){ if (scrollRef.current) { scrollRef.current.scrollTop = scrollTop; state.ignoreScrollToTop = scrollRef.current.scrollTop; } }, get targetScrollTop () { if (!scrollRef.current || !contentRef.current) { return 0; } return scrollRef.current.scrollHeight - 1 - scrollRef.current.clientHeight; }, get calculatedTargetScrollTop () { if (!scrollRef.current || !contentRef.current) { return 0; } var targetScrollTop = this.targetScrollTop; if (!optionsRef.current.targetScrollTop) { return targetScrollTop; } if ((lastCalculation === null || lastCalculation === void 0 ? void 0 : lastCalculation.targetScrollTop) === targetScrollTop) { return lastCalculation.calculatedScrollTop; } var calculatedScrollTop = Math.max(Math.min(optionsRef.current.targetScrollTop(targetScrollTop, { scrollElement: scrollRef.current, contentElement: contentRef.current }), targetScrollTop), 0); lastCalculation = { targetScrollTop: targetScrollTop, calculatedScrollTop: calculatedScrollTop }; requestAnimationFrame(function() { lastCalculation = undefined; }); return calculatedScrollTop; }, get scrollDifference () { return this.calculatedTargetScrollTop - this.scrollTop; }, get isNearBottom () { return this.scrollDifference <= STICK_TO_BOTTOM_OFFSET_PX; } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); var setIsAtBottom = useCallback(function(value) { state.isAtBottom = value; updateIsAtBottom(value); }, [ state ]); var setEscapedFromLock = useCallback(function(value) { state.escapedFromLock = value; updateEscapedFromLock(value); }, [ state ]); var scrollToBottom = useCallback(function() { var scrollOptions = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {}; var _state_animation; if (typeof scrollOptions === 'string') { scrollOptions = { animation: scrollOptions }; } if (!scrollOptions.preserveScrollPosition) { setIsAtBottom(true); } var waitElapsed = Date.now() + (Number(scrollOptions.wait) || 0); var behavior = mergeAnimations(optionsRef.current, scrollOptions.animation); var _scrollOptions_ignoreEscapes = scrollOptions.ignoreEscapes, ignoreEscapes = _scrollOptions_ignoreEscapes === void 0 ? false : _scrollOptions_ignoreEscapes; var durationElapsed; var startTarget = state.calculatedTargetScrollTop; if (_$1(scrollOptions.duration, Promise)) { scrollOptions.duration.then(function() { durationElapsed = Date.now(); }, function() { durationElapsed = Date.now(); }); } else { var _scrollOptions_duration; durationElapsed = waitElapsed + ((_scrollOptions_duration = scrollOptions.duration) !== null && _scrollOptions_duration !== void 0 ? _scrollOptions_duration : 0); } var next = function next1() { var promise = new Promise(requestAnimationFrame).then(function() { var _state_lastTick, _state; if (!state.isAtBottom) { state.animation = undefined; return false; } var scrollTop1 = state.scrollTop; var tick = performance.now(); var tickDelta = (tick - ((_state_lastTick = state.lastTick) !== null && _state_lastTick !== void 0 ? _state_lastTick : tick)) / SIXTY_FPS_INTERVAL_MS; (_state = state).animation || (_state.animation = { behavior: behavior, promise: promise, ignoreEscapes: ignoreEscapes }); if (state.animation.behavior === behavior) { state.lastTick = tick; } if (isSelecting()) { return next(); } if (waitElapsed > Date.now()) { return next(); } if (scrollTop1 < Math.min(startTarget, state.calculatedTargetScrollTop)) { var _state_animation; if (((_state_animation = state.animation) === null || _state_animation === void 0 ? void 0 : _state_animation.behavior) === behavior) { if (behavior === 'instant') { state.scrollTop = state.calculatedTargetScrollTop; return next(); } state.velocity = (behavior.damping * state.velocity + behavior.stiffness * state.scrollDifference) / behavior.mass; state.accumulated += state.velocity * tickDelta; state.scrollTop += state.accumulated; if (state.scrollTop !== scrollTop1) { state.accumulated = 0; } } return next(); } if (durationElapsed > Date.now()) { startTarget = state.calculatedTargetScrollTop; return next(); } state.animation = undefined; /** * If we're still below the target, then queue * up another scroll to the bottom with the last * requested animatino. */ if (state.scrollTop < state.calculatedTargetScrollTop) { return scrollToBottom({ animation: mergeAnimations(optionsRef.current, optionsRef.current.resize), ignoreEscapes: ignoreEscapes, duration: Math.max(0, durationElapsed - Date.now()) || undefined }); } return state.isAtBottom; }); return promise.then(function(result) { requestAnimationFrame(function() { if (!state.animation) { state.lastTick = undefined; state.velocity = 0; } }); return result; }); }; if (scrollOptions.wait !== true) { state.animation = undefined; } if (((_state_animation = state.animation) === null || _state_animation === void 0 ? void 0 : _state_animation.behavior) === behavior) { return state.animation.promise; } return next(); }, [ setIsAtBottom, isSelecting, state ]); var stopScroll = useCallback(function() { setEscapedFromLock(true); setIsAtBottom(false); }, [ setEscapedFromLock, setIsAtBottom ]); var handleScroll = useCallback(function(param) { var target = param.target; if (target !== scrollRef.current) { return; } var scrollTop1 = state.scrollTop, ignoreScrollToTop = state.ignoreScrollToTop; var _state_lastScrollTop = state.lastScrollTop, lastScrollTop = _state_lastScrollTop === void 0 ? scrollTop1 : _state_lastScrollTop; state.lastScrollTop = scrollTop1; state.ignoreScrollToTop = undefined; if (ignoreScrollToTop && ignoreScrollToTop > scrollTop1) { /** * When the user scrolls up while the animation plays, the `scrollTop` may * not come in separate events; if this happens, to make sure `isScrollingUp` * is correct, set the lastScrollTop to the ignored event. */ lastScrollTop = ignoreScrollToTop; } setIsNearBottom(state.isNearBottom); /** * Scroll events may come before a ResizeObserver event, * so in order to ignore resize events correctly we use a * timeout. * * @see https://github.com/WICG/resize-observer/issues/25#issuecomment-248757228 */ setTimeout(function() { var _state_animation; /** * When theres a resize difference ignore the resize event. */ if (state.resizeDifference || scrollTop1 === ignoreScrollToTop) { return; } if (isSelecting()) { setEscapedFromLock(true); setIsAtBottom(false); return; } var isScrollingDown = scrollTop1 > lastScrollTop; var isScrollingUp = scrollTop1 < lastScrollTop; if ((_state_animation = state.animation) === null || _state_animation === void 0 ? void 0 : _state_animation.ignoreEscapes) { state.scrollTop = lastScrollTop; return; } if (isScrollingUp) { setEscapedFromLock(true); setIsAtBottom(false); } if (isScrollingDown) { setEscapedFromLock(false); } if (!state.escapedFromLock && state.isNearBottom) { setIsAtBottom(true); } }, 1); }, [ setEscapedFromLock, setIsAtBottom, isSelecting, state ]); var handleWheel = useCallback(function(param) { var target = param.target, deltaY = param.deltaY; var _state_animation; var element = target; while(![ 'scroll', 'auto' ].includes(getComputedStyle(element).overflow)){ if (!element.parentElement) { return; } element = element.parentElement; } /** * The browser may cancel the scrolling from the mouse wheel * if we update it from the animation in meantime. * To prevent this, always escape when the wheel is scrolled up. */ if (element === scrollRef.current && deltaY < 0 && scrollRef.current.scrollHeight > scrollRef.current.clientHeight && !((_state_animation = state.animation) === null || _state_animation === void 0 ? void 0 : _state_animation.ignoreEscapes)) { setEscapedFromLock(true); setIsAtBottom(false); } }, [ setEscapedFromLock, setIsAtBottom, state ]); // Attach scroll and wheel event listeners useEffect(function() { var scroll = scrollRef.current; if (!scroll) { return undefined; } scroll.addEventListener('scroll', handleScroll, { passive: true }); scroll.addEventListener('wheel', handleWheel, { passive: true }); return function() { scroll.removeEventListener('scroll', handleScroll); scroll.removeEventListener('wheel', handleWheel); }; }, [ handleScroll, handleWheel ]); // Attach ResizeObserver to content element useEffect(function() { var content = contentRef.current; if (!content) { return undefined; } var previousHeight; var resizeObserver = new ResizeObserver(function(param) { var _param = _(param, 1), entry = _param[0]; var height = entry.contentRect.height; var difference = height - (previousHeight !== null && previousHeight !== void 0 ? previousHeight : height); state.resizeDifference = difference; /** * Sometimes the browser can overscroll past the target, * so check for this and adjust appropriately. */ if (state.scrollTop > state.targetScrollTop) { state.scrollTop = state.targetScrollTop; } setIsNearBottom(state.isNearBottom); if (difference >= 0) { /** * If it's a positive resize, scroll to the bottom when * we're already at the bottom. */ var animation = mergeAnimations(optionsRef.current, previousHeight ? optionsRef.current.resize : optionsRef.current.initial); scrollToBottom({ animation: animation, wait: true, preserveScrollPosition: true, duration: animation === 'instant' ? undefined : RETAIN_ANIMATION_DURATION_MS }); } else if (state.isNearBottom) { /** * Else if it's a negative resize, check if we're near the bottom * if we are want to un-escape from the lock, because the resize * could have caused the container to be at the bottom. */ setEscapedFromLock(false); setIsAtBottom(true); } previousHeight = height; /** * Reset the resize difference after the scroll event * has fired. Requires a rAF to wait for the scroll event, * and a setTimeout to wait for the other timeout we have in * resizeObserver in case the scroll event happens after the * resize event. */ requestAnimationFrame(function() { setTimeout(function() { if (state.resizeDifference === difference) { state.resizeDifference = 0; } }, 1); }); }); resizeObserver.observe(content); state.resizeObserver = resizeObserver; return function() { resizeObserver.disconnect(); state.resizeObserver = undefined; }; }, [ state, setIsNearBottom, setEscapedFromLock, setIsAtBottom, scrollToBottom ]); return { contentRef: contentRef, scrollRef: scrollRef, scrollToBottom: scrollToBottom, stopScroll: stopScroll, isAtBottom: isAtBottom || isNearBottom, isNearBottom: isNearBottom, escapedFromLock: escapedFromLock, state: state }; }; } var animationCache = new Map(); function mergeAnimations() { for(var _len = arguments.length, animations = new Array(_len), _key = 0; _key < _len; _key++){ animations[_key] = arguments[_key]; } var result = _$2({}, DEFAULT_SPRING_ANIMATION); var instant = false; animations.forEach(function(animation) { var _animation_damping, _animation_stiffness, _animation_mass; if (animation === 'instant') { instant = true; return; } if ((typeof animation === "undefined" ? "undefined" : _$3(animation)) !== 'object') { return; } instant = false; result.damping = (_animation_damping = animation.damping) !== null && _animation_damping !== void 0 ? _animation_damping : result.damping; result.stiffness = (_animation_stiffness = animation.stiffness) !== null && _animation_stiffness !== void 0 ? _animation_stiffness : result.stiffness; result.mass = (_animation_mass = animation.mass) !== null && _animation_mass !== void 0 ? _animation_mass : result.mass; }); var key = JSON.stringify(result); if (!animationCache.has(key)) { animationCache.set(key, Object.freeze(result)); } return instant ? 'instant' : animationCache.get(key); } export { createStickToBottom };