UNPKG

@spaced-out/ui-design-system

Version:
205 lines (188 loc) 9.01 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ScrollingLoader = exports.SCROLLING_LOADER_SIZE = void 0; var React = _interopRequireWildcard(require("react")); var _classify = _interopRequireDefault(require("../../utils/classify")); var _qa = require("../../utils/qa"); var _SemanticIcon = require("../Icon/SemanticIcon"); var _ScrollingLoaderModule = _interopRequireDefault(require("./ScrollingLoader.module.css")); var _jsxRuntime = require("react/jsx-runtime"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } const SCROLLING_LOADER_SIZE = exports.SCROLLING_LOADER_SIZE = Object.freeze({ medium: 'medium', large: 'large', fluid: 'fluid' }); const ScrollingLoader = exports.ScrollingLoader = /*#__PURE__*/React.forwardRef((_ref, ref) => { let { iconList, textList, size = SCROLLING_LOADER_SIZE.medium, classNames, testId, textAnimationDuration = 2500, loopTexts = false } = _ref; // Track active and exiting text indices for animation const [activeIndex, setActiveIndex] = React.useState(0); const [exitingIndex, setExitingIndex] = React.useState(null); // Ref to track active index for interval callback (avoids stale closure) const activeIndexRef = React.useRef(0); // Ref to track textList length for interval callback (supports streaming without restart) const textListLengthRef = React.useRef(textList.length); // Ref to track loopTexts for interval callback (avoids stale closure) const loopTextsRef = React.useRef(loopTexts); // Ref to track the timeout for clearing exiting state const exitingTimeoutRef = React.useRef(null); // Create extended icon list for infinite scroll effect with unique IDs const extendedIconList = React.useMemo(() => // Duplicate the list to create seamless looping [...iconList, ...iconList].map((icon, idx) => ({ ...icon, id: `${icon.name}-${idx < iconList.length ? 'a' : 'b'}-${idx % iconList.length}` })), [iconList]); const skeletonAnimationDuration = iconList.length * 1.5; // Create text items with unique IDs const textItems = React.useMemo(() => textList.map((text, idx) => ({ text, id: `text-${idx}-${text.slice(0, 10).replace(/\s/g, '')}`, index: idx })), [textList]); // Keep textList length ref in sync (supports streaming without restarting interval) React.useEffect(() => { textListLengthRef.current = textList.length; }, [textList.length]); // Keep loopTexts ref in sync React.useEffect(() => { loopTextsRef.current = loopTexts; }, [loopTexts]); // Reset indices when textList length decreases and current index is out of bounds // This prevents invisible text when switching to a shorter list React.useEffect(() => { if (activeIndexRef.current >= textList.length) { activeIndexRef.current = 0; setActiveIndex(0); setExitingIndex(null); } }, [textList.length]); // Animation interval - uses refs to support streaming without restart React.useEffect(() => { const interval = setInterval(() => { const currentLength = textListLengthRef.current; const shouldLoop = loopTextsRef.current; // Only animate if we have multiple items if (currentLength <= 1) { return; } const currentIndex = activeIndexRef.current; const isAtLastItem = currentIndex === currentLength - 1; // If not looping and at the last item, don't advance if (!shouldLoop && isAtLastItem) { return; } const nextIndex = (currentIndex + 1) % currentLength; // Set exiting index to current setExitingIndex(currentIndex); // Clear any existing timeout before creating a new one if (exitingTimeoutRef.current !== null) { clearTimeout(exitingTimeoutRef.current); } // Clear exiting state after animation completes exitingTimeoutRef.current = setTimeout(() => { setExitingIndex(null); }, 350); // Update ref and state activeIndexRef.current = nextIndex; setActiveIndex(nextIndex); }, textAnimationDuration); return () => { clearInterval(interval); // Clean up any pending timeout on unmount if (exitingTimeoutRef.current !== null) { clearTimeout(exitingTimeoutRef.current); } }; }, [textAnimationDuration]); return /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { ref: ref, "data-testid": (0, _qa.generateTestId)({ base: testId, slot: 'wrapper' }), className: (0, _classify.default)(_ScrollingLoaderModule.default.wrapper, { [_ScrollingLoaderModule.default.medium]: size === SCROLLING_LOADER_SIZE.medium, [_ScrollingLoaderModule.default.large]: size === SCROLLING_LOADER_SIZE.large, [_ScrollingLoaderModule.default.fluid]: size === SCROLLING_LOADER_SIZE.fluid }, classNames?.wrapper), children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { "data-testid": (0, _qa.generateTestId)({ base: testId, slot: 'skeleton' }), className: (0, _classify.default)(_ScrollingLoaderModule.default.skeletonContainer, classNames?.skeletonContainer), children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("div", { className: _ScrollingLoaderModule.default.skeletonScroller, style: { '--animation-duration': `${skeletonAnimationDuration}s` }, children: extendedIconList.map(icon => /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { className: (0, _classify.default)(_ScrollingLoaderModule.default.skeletonRow, { [_ScrollingLoaderModule.default.skeletonRowSmall]: size === SCROLLING_LOADER_SIZE.medium, [_ScrollingLoaderModule.default.skeletonRowLarge]: size === SCROLLING_LOADER_SIZE.large || size === SCROLLING_LOADER_SIZE.fluid }), "data-testid": (0, _qa.generateTestId)({ base: testId, slot: `skeleton-row-${icon.id}` }), children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_SemanticIcon.SemanticIcon, { name: icon.name, type: icon.type || 'regular', size: size === SCROLLING_LOADER_SIZE.medium ? 'small' : 'medium', semantic: "neutral", testId: (0, _qa.generateTestId)({ base: testId, slot: `icon-${icon.id}` }) }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { className: _ScrollingLoaderModule.default.skeletonBars, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("div", { className: _ScrollingLoaderModule.default.skeletonBarTitle }), /*#__PURE__*/(0, _jsxRuntime.jsx)("div", { className: _ScrollingLoaderModule.default.skeletonBarSubtitle })] })] }, icon.id)) }), /*#__PURE__*/(0, _jsxRuntime.jsx)("div", { className: _ScrollingLoaderModule.default.gradientTop }), /*#__PURE__*/(0, _jsxRuntime.jsx)("div", { className: _ScrollingLoaderModule.default.gradientBottom })] }), /*#__PURE__*/(0, _jsxRuntime.jsx)("div", { "data-testid": (0, _qa.generateTestId)({ base: testId, slot: 'text' }), className: (0, _classify.default)(_ScrollingLoaderModule.default.textContainer, classNames?.textContainer), children: /*#__PURE__*/(0, _jsxRuntime.jsx)("div", { className: _ScrollingLoaderModule.default.textScroller, children: textItems.map(item => /*#__PURE__*/(0, _jsxRuntime.jsx)("span", { className: (0, _classify.default)(_ScrollingLoaderModule.default.textItem, { [_ScrollingLoaderModule.default.textItemMedium]: size === SCROLLING_LOADER_SIZE.medium, [_ScrollingLoaderModule.default.textItemLarge]: size === SCROLLING_LOADER_SIZE.large || size === SCROLLING_LOADER_SIZE.fluid, [_ScrollingLoaderModule.default.textItemStatic]: textList.length <= 1, [_ScrollingLoaderModule.default.textItemActive]: textList.length > 1 && item.index === activeIndex, [_ScrollingLoaderModule.default.textItemExiting]: textList.length > 1 && item.index === exitingIndex }), "data-testid": (0, _qa.generateTestId)({ base: testId, slot: `text-item-${item.id}` }), children: item.text }, item.id)) }) })] }); });