@spaced-out/ui-design-system
Version:
Sense UI components library
205 lines (188 loc) • 9.01 kB
JavaScript
;
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))
})
})]
});
});