UNPKG

@razorpay/blade

Version:

The Design System that powers Razorpay

190 lines (174 loc) 8.08 kB
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