@razorpay/blade
Version:
The Design System that powers Razorpay
190 lines (174 loc) • 8.08 kB
JavaScript
import { useRef, useMemo, useEffect } from 'react';
import styled from 'styled-components';
import '../Typography/index.js';
import '../Box/BaseBox/index.js';
import { useIsMobile } from '../../utils/useIsMobile.js';
import '../../tokens/global/index.js';
import '../../utils/makeSize/index.js';
import debounce from '../../utils/lodashButBetter/debounce.js';
import { jsx, jsxs } from 'react/jsx-runtime';
import { BaseBox } from '../Box/BaseBox/BaseBox.web.js';
import { makeSize } from '../../utils/makeSize/makeSize.js';
import { size } from '../../tokens/global/size.js';
import { Text } from '../Typography/Text/Text.js';
var StyledScrollContainer = /*#__PURE__*/styled(BaseBox).withConfig({
displayName: "SpinWheelweb__StyledScrollContainer",
componentId: "sc-1gjdbly-0"
})(["scroll-snap-type:y proximity;scroll-behavior:smooth;&::-webkit-scrollbar{display:none;}scrollbar-width:none;"]);
// Styled scroll item with scroll snap
var StyledScrollItem = /*#__PURE__*/styled(BaseBox).withConfig({
displayName: "SpinWheelweb__StyledScrollItem",
componentId: "sc-1gjdbly-1"
})(["scroll-snap-align:center;"]);
/**
* Reusable SpinWheel component for time selection
* Creates a scrollable column of values where the center item is selected.
*/
var SpinWheel = function SpinWheel(_ref) {
var className = _ref.className,
values = _ref.values,
selectedValue = _ref.selectedValue,
onChange = _ref.onChange,
activeIndex = _ref.activeIndex,
onActiveIndexChange = _ref.onActiveIndexChange,
displayValue = _ref.displayValue,
scrollContainerRef = _ref.scrollContainerRef,
tabIndex = _ref.tabIndex;
var containerRef = useRef(null);
var itemRefs = useRef([]);
var programmaticScrollTimeoutRef = useRef(null);
var isMobile = useIsMobile();
// Memoized debounced onChange to avoid jerky scrolling and prevent memory leaks
var debouncedOnChange = useMemo(function () {
return debounce(function (value, index) {
onChange(value, index);
}, 150);
}, [onChange]);
// Flag to prevent onValueChange from being triggered during auto-positioning
// Problem: When minuteStep > 1 and user types "03", we auto-position to nearest step "00"
// This programmatic scroll triggers handleScroll -> onValueChange -> changes value from "03" to "00"
// But we want to preserve the typed value "03" and only visually position at "00"
// Solution: Set this flag during programmatic scrolls to prevent onValueChange calls
// preserving user's typed value while showing correct visual positioning
var isProgrammaticScroll = useRef(false);
// Use displayValue for visual positioning, selectedValue for actual data
// This supports minute steps: user types "03", displayValue shows "00" for positioning,
// but selectedValue preserves "03" for form submission
var positioningValue = displayValue !== null && displayValue !== void 0 ? displayValue : selectedValue;
// Auto-scroll to positioned item when dropdown opens or positioning value changes
useEffect(function () {
// Clear any existing programmatic scroll timeout
if (programmaticScrollTimeoutRef.current) {
clearTimeout(programmaticScrollTimeoutRef.current);
}
var positionIndex = values.findIndex(function (val) {
return String(val) === String(positioningValue);
});
if (positionIndex >= 0 && itemRefs.current[positionIndex]) {
var _itemRefs$current$pos;
isProgrammaticScroll.current = true;
(_itemRefs$current$pos = itemRefs.current[positionIndex]) === null || _itemRefs$current$pos === void 0 || _itemRefs$current$pos.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
// Reset flag after scroll finishes
programmaticScrollTimeoutRef.current = setTimeout(function () {
isProgrammaticScroll.current = false;
}, 300);
}
}, [positioningValue, values]);
// Cleanup timeouts on unmount
useEffect(function () {
return function () {
if (programmaticScrollTimeoutRef.current) {
clearTimeout(programmaticScrollTimeoutRef.current);
}
};
}, []);
// Scroll event handler to update selection based on center position
var handleScroll = function handleScroll() {
if (isProgrammaticScroll.current || !containerRef.current) return;
// Use document.elementFromPoint for efficient center detection (similar to Carousel)
var containerBB = containerRef.current.getBoundingClientRect();
var pointX = containerBB.left + containerBB.width * 0.5; // Center horizontally
var pointY = containerBB.top + containerBB.height * 0.5; // Center vertically
var element = document.elementFromPoint(pointX, pointY);
var spinWheelItem = element === null || element === void 0 ? void 0 : element.closest('[data-item-index]');
if (!spinWheelItem) return;
var itemIndex = Number(spinWheelItem.getAttribute('data-item-index'));
if (isNaN(itemIndex) || itemIndex < 0 || itemIndex >= values.length) return;
// Update active index immediately for visual feedback
onActiveIndexChange === null || onActiveIndexChange === void 0 || onActiveIndexChange(itemIndex);
// Debounce the actual value change to avoid jerky scrolling
debouncedOnChange(values[itemIndex], itemIndex);
};
var handleItemClick = function handleItemClick(value, index) {
// Always allow explicit user selection via click, even when displayValue is present
// This lets users choose to override their typed value with a step value if desired
onChange(value, index);
onActiveIndexChange === null || onActiveIndexChange === void 0 || onActiveIndexChange(index);
};
return /*#__PURE__*/jsx(BaseBox, {
className: className,
display: "flex",
flexDirection: "column",
alignItems: "center",
width: isMobile ? makeSize(size[82]) : makeSize(size[66]),
height: makeSize(size[172]),
borderRadius: "small",
children: /*#__PURE__*/jsx(BaseBox, {
position: "relative",
width: "100%",
overflow: "hidden",
children: /*#__PURE__*/jsxs(StyledScrollContainer, {
ref: function ref(node) {
containerRef.current = node;
if (typeof scrollContainerRef === 'function') {
scrollContainerRef(node);
} else if (scrollContainerRef && 'current' in scrollContainerRef) {
scrollContainerRef.current = node;
}
},
height: "100%",
overflowY: "auto",
onScroll: handleScroll,
tabIndex: typeof tabIndex === 'number' ? tabIndex : undefined,
children: [/*#__PURE__*/jsx(BaseBox, {
height: "68px"
}), values.map(function (value, index) {
// Show visual selection based on positioning value (for smooth minute steps)
// but preserve actual selectedValue for form data integrity
var isVisuallySelected = activeIndex === index || String(value) === String(positioningValue);
return /*#__PURE__*/jsx(StyledScrollItem, {
ref: function ref(el) {
return itemRefs.current[index] = el;
},
height: "34px",
display: "flex",
alignItems: "center",
justifyContent: "center",
onClick: function onClick() {
return handleItemClick(value, index);
},
style: {
cursor: 'pointer'
},
"data-item-index": index,
children: /*#__PURE__*/jsx(Text, {
variant: "body",
size: isVisuallySelected ? 'large' : 'medium',
weight: isVisuallySelected ? 'semibold' : 'regular',
color: isVisuallySelected ? 'interactive.text.gray.normal' : 'interactive.text.gray.muted',
textAlign: "center",
children: String(value).padStart(2, '0')
})
}, "".concat(value, "-").concat(index));
}), /*#__PURE__*/jsx(BaseBox, {
height: "68px"
})]
})
})
});
};
export { SpinWheel };
//# sourceMappingURL=SpinWheel.web.js.map