UNPKG

@razorpay/blade

Version:

The Design System that powers Razorpay

193 lines (174 loc) 8.68 kB
import _slicedToArray from '@babel/runtime/helpers/slicedToArray'; import { useMemo, useState, useRef, useEffect, useCallback } from 'react'; /** * Formats user input according to pattern. format("1234", "##/##") → "12/34" */ var format = function format(value, pattern) { if (!pattern) return value; var result = ''; var valueIndex = 0; for (var i = 0; i < pattern.length; i++) { var patternChar = pattern[i]; // "#" or "/" if (patternChar === '#') { if (valueIndex < value.length) { result += value[valueIndex]; // add "1" from "1234" valueIndex++; } else { break; // No more input chars, stop } } else { result += patternChar; // add "/" delimiter } } return result; // "12/34" }; /** * Removes delimiters, keeps only user input. stripPatternCharacters("12/34") → "1234" */ var stripPatternCharacters = function stripPatternCharacters(value) { return value.replace(/[^\dA-z]/g, ''); // "12/34" → "1234" (removes "/") }; /** * Checks if character is user input vs delimiter. isUserCharacter('1') → true, isUserCharacter('/') → false */ var isUserCharacter = function isUserCharacter(character) { return /[\dA-z]/.test(character); // "1" → true, "/" → false }; /** * Hook for pattern-based input formatting with smart cursor positioning. * useFormattedInput({ format: "##/##" }) transforms "1234" → "12/34" */ var useFormattedInput = function useFormattedInput(_ref) { var pattern = _ref.format, onChange = _ref.onChange, userValue = _ref.value, _ref$defaultValue = _ref.defaultValue, defaultValue = _ref$defaultValue === void 0 ? '' : _ref$defaultValue; var initialValue = useMemo(function () { return format(userValue !== null && userValue !== void 0 ? userValue : defaultValue, pattern !== null && pattern !== void 0 ? pattern : ''); }, [userValue, defaultValue, pattern]); var _useState = useState(initialValue), _useState2 = _slicedToArray(_useState, 2), internalValue = _useState2[0], setInternalValue = _useState2[1]; var inputRef = useRef(null); var infoRef = useRef({}); var maxLength = useMemo(function () { return pattern === null || pattern === void 0 ? void 0 : pattern.length; }, [pattern]); // Reset internal state when parent clears value (form resets, external state changes) // Preserves format delimiters for visual guidance. Example: "(###)" → "( )" when cleared useEffect(function () { if ((userValue === '' || userValue === undefined) && defaultValue === '') { var emptyFormatted = format('', pattern !== null && pattern !== void 0 ? pattern : ''); setInternalValue(emptyFormatted); } // DATEPICKER FIX: Sync internal state when external value changes // This addresses the issue where DatePicker programmatically updates the value prop // (e.g., when user selects date from calendar), but the formatted input's internal // state doesn't update, causing the input to not reflect the new value. // Without this, only user typing and empty resets were handled. if (userValue !== undefined && userValue !== '' && pattern) { var rawValue = stripPatternCharacters(userValue); var newFormatted = format(rawValue, pattern); // Only update if the formatted value actually changed to avoid unnecessary re-renders if (newFormatted !== internalValue) { setInternalValue(newFormatted); } } }, [userValue, pattern]); // Apply calculated cursor position after value updates useEffect(function () { var _infoRef$current = infoRef.current, cursorPosition = _infoRef$current.cursorPosition, endOfSection = _infoRef$current.endOfSection; if (endOfSection || cursorPosition === undefined) return; // Skip if no position or end section if (inputRef.current) { inputRef.current.setSelectionRange(cursorPosition, cursorPosition); } }, [internalValue]); var handleChange = useCallback(function (_ref2) { var _inputRef$current$sel, _inputRef$current; var name = _ref2.name, inputValue = _ref2.value; if (!pattern) { // No pattern = regular input var cleanValue = inputValue !== null && inputValue !== void 0 ? inputValue : ''; onChange === null || onChange === void 0 || onChange({ name: name, value: cleanValue }); setInternalValue(cleanValue); return; } var currentValue = internalValue; // "12/34" (user wants to delete "/") var newInputValue = inputValue !== null && inputValue !== void 0 ? inputValue : ''; // "1234" (after deleting "/") var cursorPosition = (_inputRef$current$sel = (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 ? void 0 : _inputRef$current.selectionStart) !== null && _inputRef$current$sel !== void 0 ? _inputRef$current$sel : 0; // 2 (cursor where "/" was) var didDelete = newInputValue.length < currentValue.length; // 4 < 5 → true infoRef.current.cursorPosition = cursorPosition; var rawValue = stripPatternCharacters(newInputValue); // "1234" → "1234" // Handle special case: user deleted a delimiter (like deleting "/" in "12/|34") if (didDelete) { var _currentValue$cursorP; var deletedChar = (_currentValue$cursorP = currentValue[cursorPosition]) !== null && _currentValue$cursorP !== void 0 ? _currentValue$cursorP : ''; // "12/34"[2] → "/" var deletedDelimiter = !isUserCharacter(deletedChar); // "/" → true (is delimiter) if (deletedDelimiter) { // true (will execute for "/" deletion) var beforeCursor = newInputValue.substring(0, cursorPosition); // "12" (before cursor) var afterCursor = newInputValue.substring(cursorPosition); // "34" (after cursor) var rawBefore = stripPatternCharacters(beforeCursor); // "12" → "12" var rawAfter = stripPatternCharacters(afterCursor); // "34" → "34" rawValue = rawBefore.slice(0, -1) + rawAfter; // "12".slice(0,-1) + "34" → "1" + "34" → "134" // Removes trailing non-alphanumeric characters from the end of the string, preserving the last alphanumeric word before them. infoRef.current.cursorPosition = beforeCursor.replace(/([\d\w]+)[^\dA-z]+$/, '$1').length - 1; } } var formattedValue = format(rawValue, pattern); // format("134", "##/##") → "13/4" infoRef.current.endOfSection = false; // Handle cursor positioning when typing (not deleting) if (!didDelete) { // User types "2" in "1|" → becomes "12|/" → should jump to "12/|" var nextChar = formattedValue[cursorPosition]; // "12/"[2] → "/" (delimiter) var nextIsDelimiter = nextChar ? !isUserCharacter(nextChar) : false; // "/" → true var remainingText = formattedValue.substring(cursorPosition); // "12/".substring(2) → "/" var nextUserCharIndex = remainingText.search(/[\dA-z]/); // "/".search() → -1 (no user chars) var hasMoreUserChars = nextUserCharIndex !== -1; // -1 !== -1 → false infoRef.current.endOfSection = nextIsDelimiter && !hasMoreUserChars; // true && false → false // Move cursor past auto-inserted delimiters for smooth typing if (nextIsDelimiter && hasMoreUserChars) { var _formattedValue; var prevChar = (_formattedValue = formattedValue[cursorPosition - 1]) !== null && _formattedValue !== void 0 ? _formattedValue : ''; var prevIsDelimiter = !isUserCharacter(prevChar); if (prevIsDelimiter) { infoRef.current.cursorPosition = cursorPosition + nextUserCharIndex + 1; } else { // If we're at a delimiter after typing (not deleting), and there are more chars, // we probably need to move past it unless it's a brand new delimiter var delimiterExistedBefore = currentValue[cursorPosition] === formattedValue[cursorPosition]; if (delimiterExistedBefore) { infoRef.current.cursorPosition = cursorPosition + 1; } } } } onChange === null || onChange === void 0 || onChange({ name: name, value: formattedValue, rawValue: rawValue }); setInternalValue(formattedValue); }, [pattern, onChange, internalValue]); var handleKeyDown = useCallback(function (event) { if (event.currentTarget && inputRef.current !== event.currentTarget) { inputRef.current = event.currentTarget; } }, []); return { formattedValue: internalValue, handleChange: handleChange, handleKeyDown: handleKeyDown, maxLength: maxLength }; }; export { useFormattedInput }; //# sourceMappingURL=useFormattedInput.js.map