@razorpay/blade
Version:
The Design System that powers Razorpay
193 lines (174 loc) • 8.68 kB
JavaScript
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