From 10cd1db154bbb5df66eecfae796fe42e7ae4470c Mon Sep 17 00:00:00 2001 From: harshpathakzz Date: Fri, 21 Mar 2025 13:28:47 +0530 Subject: [PATCH 1/7] fixed ref prop --- .../atoms/DataRowInput/DataRowInput.tsx | 33 +++++++---- .../atoms/FreeFormInput/FreeFormInput.tsx | 37 +++++++----- .../atoms/FreeFormInput/styles/index.css | 3 + .../atoms/InputStepper/InputStepper.tsx | 58 +++++++++++-------- .../stories/FreeFormInput.stories.tsx | 4 +- .../stories/InputStepper.stories.tsx | 7 +++ 6 files changed, 93 insertions(+), 49 deletions(-) diff --git a/packages/ui-toolkit/src/components/atoms/DataRowInput/DataRowInput.tsx b/packages/ui-toolkit/src/components/atoms/DataRowInput/DataRowInput.tsx index caa9c003..8f5be2f8 100644 --- a/packages/ui-toolkit/src/components/atoms/DataRowInput/DataRowInput.tsx +++ b/packages/ui-toolkit/src/components/atoms/DataRowInput/DataRowInput.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, forwardRef } from 'react'; import cn from 'classnames'; import type { ReactIconComponentType } from '@groww-tech/icon-store'; import { ContentMintTokens } from '../../../types/mint-token-types/content-mint-tokens'; @@ -14,7 +14,6 @@ export type DataRowInputProps = { width?: string; PrefixIcon?: ReactIconComponentType; prefixLabel?: string; - ref?: React.RefObject; disabled?: boolean; error?: boolean; warning?: boolean; @@ -32,10 +31,11 @@ export type DataRowInputProps = { backgroundColor?: BackgroundMintTokens; disableCopyPaste?: boolean; onEnterPress?: (e: React.KeyboardEvent) => void; + onFocus?: (e: React.FocusEvent) => void; + onBlur?: (e: React.FocusEvent) => void; } - -const DataRowInput: React.FC = ({ +const DataRowInput = forwardRef(({ placeholder, value, onChange, @@ -43,7 +43,6 @@ const DataRowInput: React.FC = ({ width = '128px', PrefixIcon, prefixLabel, - ref, disabled = false, error = false, warning = false, @@ -60,8 +59,10 @@ const DataRowInput: React.FC = ({ backgroundColor = 'backgroundPrimary', borderColor = 'borderPrimary', disableCopyPaste = false, - onEnterPress -}) => { + onEnterPress, + onFocus, + onBlur +}, ref) => { const [ isFocused, setIsFocused ] = useState(false); const inputClasses = cn('datarow-input', textAlign); @@ -92,6 +93,18 @@ const DataRowInput: React.FC = ({ } }; + + const handleFocus = (e: React.FocusEvent) => { + setIsFocused(true); + onFocus && onFocus(e); + }; + + + const handleBlur = (e: React.FocusEvent) => { + setIsFocused(false); + onBlur && onBlur(e); + }; + const inputContentClasses = cn( `datarow-inputContent ${textColor} ${borderColor}`, { @@ -150,8 +163,8 @@ const DataRowInput: React.FC = ({ placeholder={placeholder} value={value} onChange={onChange} - onFocus={() => setIsFocused(true)} - onBlur={() => setIsFocused(false)} + onFocus={handleFocus} + onBlur={handleBlur} disabled={disabled} data-test-id={dataTestId} ref={ref} @@ -168,6 +181,6 @@ const DataRowInput: React.FC = ({ ); -}; +}); export default DataRowInput; diff --git a/packages/ui-toolkit/src/components/atoms/FreeFormInput/FreeFormInput.tsx b/packages/ui-toolkit/src/components/atoms/FreeFormInput/FreeFormInput.tsx index 37dfb9e4..f4fece18 100644 --- a/packages/ui-toolkit/src/components/atoms/FreeFormInput/FreeFormInput.tsx +++ b/packages/ui-toolkit/src/components/atoms/FreeFormInput/FreeFormInput.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, forwardRef } from 'react'; import cn from 'classnames'; import { MdsIcCancelCircle, @@ -34,7 +34,6 @@ export type FreeFormInputProps = { error?: boolean; errorMessage?: string; clearable?: boolean; - ref?: React.RefObject; helperText?: string; helperTextColor?: ContentMintTokens; variant?: 'text' | 'password' | 'number'; @@ -51,11 +50,11 @@ export type FreeFormInputProps = { suffixIconButtonColor?: ContentMintTokens; clearIconColor?: ContentMintTokens; passwordToggleIconColor?: ContentMintTokens; - + onFocus?: (e: React.FocusEvent) => void; + onBlur?: (e: React.FocusEvent) => void; }; - -const FreeFormInput: React.FC = ({ +const FreeFormInput = forwardRef(({ placeholder, value, label, @@ -72,7 +71,6 @@ const FreeFormInput: React.FC = ({ error = false, errorMessage = '', clearable = false, - ref, helperText, helperTextColor = 'contentSecondary', variant = 'text', @@ -88,8 +86,10 @@ const FreeFormInput: React.FC = ({ suffixIconColor = 'contentSecondary', suffixIconButtonColor = 'contentSecondary', clearIconColor = 'contentSecondary', - passwordToggleIconColor = 'contentSecondary' -}) => { + passwordToggleIconColor = 'contentSecondary', + onFocus, + onBlur +}, ref) => { const [ showClearIcon, setShowClearIcon ] = useState(false); const [ isFocused, setIsFocused ] = useState(false); const [ showPassword, setShowPassword ] = useState(false); @@ -140,7 +140,6 @@ const FreeFormInput: React.FC = ({ const togglePasswordVisibility = () => { - setShowPassword(!showPassword); }; @@ -156,6 +155,18 @@ const FreeFormInput: React.FC = ({ } }; + + const handleFocus = (e: React.FocusEvent) => { + setIsFocused(true); + onFocus && onFocus(e); + }; + + + const handleBlur = (e: React.FocusEvent) => { + setIsFocused(false); + onBlur && onBlur(e); + }; + return (
= ({ { label && (
{label} @@ -212,8 +223,8 @@ const FreeFormInput: React.FC = ({ placeholder={placeholder} value={value} onChange={onChange} - onFocus={() => setIsFocused(true)} - onBlur={() => setIsFocused(false)} + onFocus={handleFocus} + onBlur={handleBlur} disabled={disabled} data-test-id={dataTestId} maxLength={maxLength} @@ -318,6 +329,6 @@ const FreeFormInput: React.FC = ({ }
); -}; +}); export default FreeFormInput; diff --git a/packages/ui-toolkit/src/components/atoms/FreeFormInput/styles/index.css b/packages/ui-toolkit/src/components/atoms/FreeFormInput/styles/index.css index ee4f9f81..cdcdb277 100644 --- a/packages/ui-toolkit/src/components/atoms/FreeFormInput/styles/index.css +++ b/packages/ui-toolkit/src/components/atoms/FreeFormInput/styles/index.css @@ -92,4 +92,7 @@ input::placeholder { .freeform-helperText { padding-left: 1px; +} +.freeform-label{ + padding-left: 1px; } \ No newline at end of file diff --git a/packages/ui-toolkit/src/components/atoms/InputStepper/InputStepper.tsx b/packages/ui-toolkit/src/components/atoms/InputStepper/InputStepper.tsx index df66b5b2..4341a63c 100644 --- a/packages/ui-toolkit/src/components/atoms/InputStepper/InputStepper.tsx +++ b/packages/ui-toolkit/src/components/atoms/InputStepper/InputStepper.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, forwardRef } from 'react'; import cn from 'classnames'; import type { ReactIconComponentType } from '@groww-tech/icon-store'; import { MdsIcRemoveMinus, MdsIcAddPlus } from '@groww-tech/icon-store/mint-icons'; @@ -7,7 +7,6 @@ import { ContentMintTokens } from '../../../types/mint-token-types/content-mint- import { BackgroundMintTokens } from '../../../types/mint-token-types/background-mint-tokens'; import './styles/index.css'; - export type InputStepperProps = { placeholder?: string; value: number; @@ -16,7 +15,6 @@ export type InputStepperProps = { width?: string; prefixIcon?: ReactIconComponentType; prefixLabel?: string; - ref?: React.RefObject; error?: boolean; warning?: boolean; disabled?: boolean; @@ -33,10 +31,11 @@ export type InputStepperProps = { disableCopyPaste?: boolean; onEnterPress?: (e: React.KeyboardEvent) => void; disableDecimal?: boolean; -} - + onFocus?: (e: React.FocusEvent) => void; + onBlur?: (e: React.FocusEvent) => void; +}; -const InputStepper: React.FC = ({ +const InputStepper = forwardRef(({ placeholder, value, onChange, @@ -44,7 +43,6 @@ const InputStepper: React.FC = ({ width = '128px', prefixIcon, prefixLabel, - ref, error = false, warning = false, disabled = false, @@ -60,23 +58,24 @@ const InputStepper: React.FC = ({ shouldFocusOnMount = false, disableCopyPaste = false, onEnterPress, - disableDecimal = false -}) => { + disableDecimal = false, + onFocus, + onBlur +}, ref) => { const [ isFocused, setIsFocused ] = useState(false); const [ inputValue, setInputValue ] = useState(value.toString()); - const internalRef = useRef(null); - const inputRef = ref || internalRef; - + // Update inputValue when value prop changes. useEffect(() => { setInputValue(value.toString()); }, [ value ]); + // Focus on mount only if a parent ref is provided and it's a mutable ref object. useEffect(() => { - if (shouldFocusOnMount && inputRef.current) { - inputRef.current.focus(); + if (shouldFocusOnMount && ref && typeof ref !== 'function' && ref.current) { + ref.current.focus(); } - }, [ inputRef, shouldFocusOnMount ]); + }, [ shouldFocusOnMount, ref ]); const inputClasses = cn('inputStepper-input width100 center-align', { contentDisabled: disabled @@ -126,7 +125,6 @@ const InputStepper: React.FC = ({ const handleChange = (e: React.ChangeEvent) => { if (!typeable) return; - const newValue = e.target.value; if (newValue === '') { @@ -144,7 +142,7 @@ const InputStepper: React.FC = ({ }; - const handleBlur = () => { + const handleBlur = (e: React.FocusEvent) => { setIsFocused(false); if (inputValue === '') { setInputValue('0'); @@ -156,6 +154,16 @@ const InputStepper: React.FC = ({ setInputValue(numValue.toString()); onChange(numValue); } + + onBlur && onBlur(e); + + }; + + + const handleFocus = (e: React.FocusEvent) => { + setIsFocused(true); + onFocus && onFocus(e); + }; @@ -176,11 +184,11 @@ const InputStepper: React.FC = ({ return (
= ({ { (prefixIcon || prefixLabel) && (
{ prefixIcon && (
{prefixIcon} @@ -215,7 +223,7 @@ const InputStepper: React.FC = ({ { prefixLabel && (
{prefixLabel} @@ -234,11 +242,11 @@ const InputStepper: React.FC = ({ placeholder={placeholder} value={inputValue} onChange={handleChange} - onFocus={() => setIsFocused(true)} + onFocus={handleFocus} onBlur={handleBlur} disabled={disabled} data-test-id={dataTestId} - ref={inputRef} + ref={ref} onWheel={handleWheel} readOnly={!typeable} onKeyDown={handleKeyDown} @@ -263,6 +271,6 @@ const InputStepper: React.FC = ({
); -}; +}); export default InputStepper; diff --git a/packages/ui-toolkit/stories/FreeFormInput.stories.tsx b/packages/ui-toolkit/stories/FreeFormInput.stories.tsx index 43338b20..5693ed77 100644 --- a/packages/ui-toolkit/stories/FreeFormInput.stories.tsx +++ b/packages/ui-toolkit/stories/FreeFormInput.stories.tsx @@ -36,7 +36,9 @@ Default.args = { errorMessage: '', disabled: false, clearable: false, - helperText: 'Helper text here' + helperText: 'Helper text here', + onFocus: () => console.log('Focused'), + onBlur: () => console.log('Blurred'), }; export const WithError = Template.bind({}); diff --git a/packages/ui-toolkit/stories/InputStepper.stories.tsx b/packages/ui-toolkit/stories/InputStepper.stories.tsx index cd73e360..2132acf9 100644 --- a/packages/ui-toolkit/stories/InputStepper.stories.tsx +++ b/packages/ui-toolkit/stories/InputStepper.stories.tsx @@ -36,6 +36,13 @@ Default.args = { value: 0, width: '128px' }; +export const FocusOnMount = Template.bind({}); +FocusOnMount.args = { + placeholder: '0', + value: 0, + width: '128px', + shouldFocusOnMount: true +}; export const WithError = Template.bind({}); WithError.args = { From 3b641b8cd6cf46fee4767167e4fab5dbc42be0da Mon Sep 17 00:00:00 2001 From: harshpathakzz Date: Fri, 21 Mar 2025 16:14:38 +0530 Subject: [PATCH 2/7] fixed e in number --- .../src/components/atoms/DataRowInput/DataRowInput.tsx | 5 +++++ .../src/components/atoms/FreeFormInput/FreeFormInput.tsx | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/packages/ui-toolkit/src/components/atoms/DataRowInput/DataRowInput.tsx b/packages/ui-toolkit/src/components/atoms/DataRowInput/DataRowInput.tsx index 8f5be2f8..9bac341c 100644 --- a/packages/ui-toolkit/src/components/atoms/DataRowInput/DataRowInput.tsx +++ b/packages/ui-toolkit/src/components/atoms/DataRowInput/DataRowInput.tsx @@ -83,6 +83,10 @@ const DataRowInput = forwardRef(({ onEnterPress(e); } + if (e.key === 'e' || e.key === 'E') { + e.preventDefault(); + } + onKeyDown && onKeyDown(e); }; @@ -116,6 +120,7 @@ const DataRowInput = forwardRef(({ 'backgroundSecondary contentSecondary': disabled } ); + console.log('Ref', ref); return (
(({ if (disableDecimal && (e.key === '.')) { e.preventDefault(); } + + if (variant === 'number') { + if (e.key === 'e' || e.key === 'E') { + e.preventDefault(); + } + + } }; From ba157d9abe03834df0582765c2b0ea3a1373dea6 Mon Sep 17 00:00:00 2001 From: harshpathakzz Date: Fri, 21 Mar 2025 16:19:40 +0530 Subject: [PATCH 3/7] updated version --- packages/ui-toolkit/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui-toolkit/package.json b/packages/ui-toolkit/package.json index e50d1d57..63e837e7 100644 --- a/packages/ui-toolkit/package.json +++ b/packages/ui-toolkit/package.json @@ -1,6 +1,6 @@ { "name": "@groww-tech/ui-toolkit", - "version": "0.8.0", + "version": "0.8.0.beta.1ß", "description": "A lightning nature UI", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", From fcfecaece5be3f846b1f7ab188231d127511716e Mon Sep 17 00:00:00 2001 From: harshpathakzz Date: Mon, 24 Mar 2025 09:41:59 +0530 Subject: [PATCH 4/7] refactored code --- packages/ui-toolkit/package.json | 2 +- .../atoms/DataRowInput/DataRowInput.tsx | 150 ++++--- .../atoms/FreeFormInput/FreeFormInput.tsx | 374 +++++++++++------- .../atoms/InputStepper/InputStepper.tsx | 234 +++++------ 4 files changed, 437 insertions(+), 323 deletions(-) diff --git a/packages/ui-toolkit/package.json b/packages/ui-toolkit/package.json index 63e837e7..0abce5ca 100644 --- a/packages/ui-toolkit/package.json +++ b/packages/ui-toolkit/package.json @@ -1,6 +1,6 @@ { "name": "@groww-tech/ui-toolkit", - "version": "0.8.0.beta.1ß", + "version": "0.8.0-beta.1", "description": "A lightning nature UI", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", diff --git a/packages/ui-toolkit/src/components/atoms/DataRowInput/DataRowInput.tsx b/packages/ui-toolkit/src/components/atoms/DataRowInput/DataRowInput.tsx index 9bac341c..c647aae7 100644 --- a/packages/ui-toolkit/src/components/atoms/DataRowInput/DataRowInput.tsx +++ b/packages/ui-toolkit/src/components/atoms/DataRowInput/DataRowInput.tsx @@ -1,4 +1,9 @@ -import React, { useState, forwardRef } from 'react'; +import React, { + useState, + forwardRef, + useCallback, + memo +} from 'react'; import cn from 'classnames'; import type { ReactIconComponentType } from '@groww-tech/icon-store'; import { ContentMintTokens } from '../../../types/mint-token-types/content-mint-tokens'; @@ -35,6 +40,49 @@ export type DataRowInputProps = { onBlur?: (e: React.FocusEvent) => void; } +// Memoized prefix component to prevent unnecessary re-renders +const PrefixComponent = memo(({ + PrefixIcon, + prefixLabel, + prefixIconColor, + perfixTextColor, + dataTestId +}: { + PrefixIcon?: ReactIconComponentType; + prefixLabel?: string; + prefixIconColor: ContentMintTokens; + perfixTextColor: ContentMintTokens; + dataTestId?: string; +}) => ( +
+ { + PrefixIcon && ( +
+ +
+ ) + } + { + prefixLabel && ( +
+ {prefixLabel} +
+ ) + } +
+)); + +PrefixComponent.displayName = 'PrefixComponent'; + const DataRowInput = forwardRef(({ placeholder, value, @@ -65,105 +113,91 @@ const DataRowInput = forwardRef(({ }, ref) => { const [ isFocused, setIsFocused ] = useState(false); - const inputClasses = cn('datarow-input', textAlign); - const inputWrapperClasses = cn('datarow-inputWrapper'); - + const hasPrefix = Boolean(PrefixIcon || prefixLabel); - const handleWheel = (e: React.WheelEvent) => { + // Memoize event handlers to prevent unnecessary re-renders + const handleWheel = useCallback((e: React.WheelEvent) => { e.currentTarget.blur(); - }; + }, []); - - const handleKeyDown = (e: React.KeyboardEvent) => { + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + // Only block the period when disableDecimal is true if (disableDecimal && e.key === '.') { e.preventDefault(); + return; } - if (e.key === 'Enter' && onEnterPress) { - onEnterPress(e); - } + // Allow navigation/control keys (backspace, delete, arrows, etc.) + const allowedKeys = [ 'Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter', 'Home', 'End' ]; - if (e.key === 'e' || e.key === 'E') { + // If it's not a digit, not a period, and not in the allowed keys list, block it + if (!/^[0-9]$/.test(e.key) && e.key !== '.' && !allowedKeys.includes(e.key)) { e.preventDefault(); + return; } - onKeyDown && onKeyDown(e); - }; + if (e.key === 'Enter' && onEnterPress) { + onEnterPress(e); + } + onKeyDown?.(e); + }, [ disableDecimal, onEnterPress, onKeyDown ]); - const handleCopyPaste = (e: React.ClipboardEvent) => { + const handleCopyPaste = useCallback((e: React.ClipboardEvent) => { if (disableCopyPaste) { e.preventDefault(); } - }; - + }, [ disableCopyPaste ]); - const handleFocus = (e: React.FocusEvent) => { + const handleFocus = useCallback((e: React.FocusEvent) => { setIsFocused(true); - onFocus && onFocus(e); - }; - + onFocus?.(e); + }, [ onFocus ]); - const handleBlur = (e: React.FocusEvent) => { + const handleBlur = useCallback((e: React.FocusEvent) => { setIsFocused(false); - onBlur && onBlur(e); - }; + onBlur?.(e); + }, [ onBlur ]); + // Memoize class names to avoid recalculation on each render const inputContentClasses = cn( `datarow-inputContent ${textColor} ${borderColor}`, { [backgroundColor]: !disabled, 'datarow-inputBorderNegative': error, 'datarow-inputBorderWarning': warning, - 'datarow-inputPrefix': PrefixIcon || prefixLabel, + 'datarow-inputPrefix': hasPrefix, 'datarow-inputFocused': isFocused && !disabled && !error, 'backgroundSecondary contentSecondary': disabled } ); - console.log('Ref', ref); + + const inputClasses = `datarow-input ${textAlign} ${textStyle} ${textColor} datarow-contentPrimary`; + return (
{ - (PrefixIcon || prefixLabel) && ( -
- { - PrefixIcon && ( -
- {/* Hardcoding size to 20 to maintain consistency across different icons and elements */} - -
- ) - } - { - prefixLabel && ( -
- {prefixLabel} -
- ) - } -
+ hasPrefix && ( + ) } (({ ); }); -export default DataRowInput; +export default memo(DataRowInput); diff --git a/packages/ui-toolkit/src/components/atoms/FreeFormInput/FreeFormInput.tsx b/packages/ui-toolkit/src/components/atoms/FreeFormInput/FreeFormInput.tsx index 80174721..cb3d7d05 100644 --- a/packages/ui-toolkit/src/components/atoms/FreeFormInput/FreeFormInput.tsx +++ b/packages/ui-toolkit/src/components/atoms/FreeFormInput/FreeFormInput.tsx @@ -1,4 +1,10 @@ -import React, { useState, useEffect, forwardRef } from 'react'; +import React, { + useState, + useEffect, + forwardRef, + useCallback, + memo +} from 'react'; import cn from 'classnames'; import { MdsIcCancelCircle, @@ -12,6 +18,156 @@ import { ContentMintTokens } from '../../../types/mint-token-types/content-mint- import './styles/index.css'; +type PrefixSectionProps = { + PrefixIcon?: ReactIconComponentType; + prefixLabel?: string; + prefixIconColor: ContentMintTokens; + perfixTextColor: ContentMintTokens; + prefixTextStyle: 'bodyBase' | 'bodyBaseHeavy'; + dataTestId?: string; +}; + +const PrefixSection = memo(({ + PrefixIcon, + prefixLabel, + prefixIconColor, + perfixTextColor, + prefixTextStyle, + dataTestId +}) => { + if (!PrefixIcon && !prefixLabel) return null; + + return ( +
+ { + PrefixIcon && ( +
+ +
+ ) + } + { + prefixLabel && ( +
+ {prefixLabel} +
+ ) + } +
+ ); +}); + + +type SuffixSectionProps = { + clearable: boolean; + showClearIcon: boolean; + variant: 'text' | 'password' | 'number'; + showPassword: boolean; + setShowPassword: React.Dispatch>; + SuffixIcon?: ReactIconComponentType; + suffixIconButton?: SuffixIconButtonProps; + handleClear: () => void; + clearIconColor: ContentMintTokens; + passwordToggleIconColor: ContentMintTokens; + suffixIconColor: ContentMintTokens; + suffixIconButtonColor: ContentMintTokens; + disabled?: boolean; + dataTestId?: string; +}; + +const SuffixSection = memo(({ + clearable, + showClearIcon, + variant, + showPassword, + setShowPassword, + SuffixIcon, + suffixIconButton, + handleClear, + clearIconColor, + passwordToggleIconColor, + suffixIconColor, + suffixIconButtonColor, + disabled, + dataTestId +}) => { + const showSuffixContainer = (clearable && showClearIcon) || variant === 'password' || !!SuffixIcon || !!suffixIconButton; + + if (!showSuffixContainer) return null; + + return ( +
+ { + clearable && showClearIcon && ( +
+ +
+ ) + } + + { + variant === 'password' && ( +
+ setShowPassword(!showPassword)} + Icon={showPassword ? MdsIcHideEye : MdsIcShowEye} + size="medium" + data-test-id={`${dataTestId}-password-toggle-button`} + iconColor={passwordToggleIconColor} + /> +
+ ) + } + + { + SuffixIcon && ( +
+ +
+ ) + } + + { + suffixIconButton && ( +
+ +
+ ) + } +
+ ); +}); + + type SuffixIconButtonProps = { icon: ReactIconComponentType; onClick: () => void; @@ -94,13 +250,13 @@ const FreeFormInput = forwardRef(({ const [ isFocused, setIsFocused ] = useState(false); const [ showPassword, setShowPassword ] = useState(false); + // Update clear icon visibility when value changes useEffect(() => { - setShowClearIcon(!!clearable && value.length > 0); + setShowClearIcon(clearable && value.length > 0); }, [ clearable, value ]); - const inputClasses = cn('freeform-input'); - const inputWrapperClasses = cn('freeform-inputWrapper flex width100'); - const inputContentClasses = cn('freeform-inputContent contentPrimary borderPrimary', { + // Memoize class computation to prevent recalculation on every render + const inputContentClasses = React.useMemo(() => cn('freeform-inputContent contentPrimary borderPrimary', { 'backgroundPrimary': !disabled, 'freeform-inputBorderNegative': error, 'freeform-inputClearable': clearable, @@ -108,17 +264,9 @@ const FreeFormInput = forwardRef(({ 'freeform-inputSuffix': SuffixIcon || (clearable && showClearIcon) || variant === 'password', 'freeform-inputFocused': isFocused && !disabled && !error, 'backgroundSecondary contentSecondary': disabled - }); - - - const handleWheel = (e: React.WheelEvent) => { - if (variant === 'number') { - e.currentTarget.blur(); - } - }; + }), [ disabled, error, clearable, PrefixIcon, prefixLabel, SuffixIcon, showClearIcon, variant, isFocused ]); - - const handleClear = () => { + const handleClear = useCallback(() => { if (onChange) { const event = { target: { value: '' }, @@ -129,55 +277,57 @@ const FreeFormInput = forwardRef(({ onChange(event); } - }; - - - const handleCopyPaste = (e: React.ClipboardEvent) => { - if (disableCopyPaste) { - e.preventDefault(); - } - }; - + }, [ onChange ]); - const togglePasswordVisibility = () => { - setShowPassword(!showPassword); - }; + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + onKeyDown?.(e); - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (onKeyDown) onKeyDown(e); if (e.key === 'Enter' && onEnterPress) { onEnterPress(e); } - if (disableDecimal && (e.key === '.')) { + // Only block decimal point when disableDecimal is true + if (disableDecimal && e.key === '.') { e.preventDefault(); + return; } + // For number variant, only prevent non-numeric input with exceptions for navigation keys if (variant === 'number') { - if (e.key === 'e' || e.key === 'E') { + const allowedKeys = [ 'Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter', 'Home', 'End', '.' ]; + + if (!/^[0-9]$/.test(e.key) && !allowedKeys.includes(e.key)) { e.preventDefault(); } - } - }; + }, [ onKeyDown, onEnterPress, disableDecimal, variant ]); - - const handleFocus = (e: React.FocusEvent) => { + const handleFocus = useCallback((e: React.FocusEvent) => { setIsFocused(true); - onFocus && onFocus(e); - }; - + onFocus?.(e); + }, [ onFocus ]); - const handleBlur = (e: React.FocusEvent) => { + const handleBlur = useCallback((e: React.FocusEvent) => { setIsFocused(false); - onBlur && onBlur(e); - }; + onBlur?.(e); + }, [ onBlur ]); + + const handleCopyPaste = useCallback((e: React.ClipboardEvent) => { + if (disableCopyPaste) { + e.preventDefault(); + } + }, [ disableCopyPaste ]); + + const handleWheel = useCallback((e: React.WheelEvent) => { + if (variant === 'number') { + e.currentTarget.blur(); + } + }, [ variant ]); return (
{ @@ -190,42 +340,21 @@ const FreeFormInput = forwardRef(({
) } -
- { - (PrefixIcon || prefixLabel) && ( -
- { - PrefixIcon && ( -
- {/* Hardcoding size to 20 to maintain consistency across different icons and elements */} - -
- ) - } - { - prefixLabel && ( -
- {prefixLabel} -
- ) - } -
- ) - } + + (({ onCut={handleCopyPaste} onPaste={handleCopyPaste} /> - { - (clearable && showClearIcon) || variant === 'password' || SuffixIcon || suffixIconButton ? ( -
- { - clearable && showClearIcon && ( -
- -
- ) - } - { - variant === 'password' && ( -
- -
- ) - } - { - SuffixIcon && ( -
- {/* Hardcoding size to 20 to maintain consistency across different icons and elements */} - -
- ) - } - { - suffixIconButton && ( -
- -
- ) - } -
- ) : null - } + +
+ { helperText && (
(({
) } + { error && errorMessage && (
@@ -338,4 +418,4 @@ const FreeFormInput = forwardRef(({ ); }); -export default FreeFormInput; +export default memo(FreeFormInput); diff --git a/packages/ui-toolkit/src/components/atoms/InputStepper/InputStepper.tsx b/packages/ui-toolkit/src/components/atoms/InputStepper/InputStepper.tsx index 4341a63c..f352ac50 100644 --- a/packages/ui-toolkit/src/components/atoms/InputStepper/InputStepper.tsx +++ b/packages/ui-toolkit/src/components/atoms/InputStepper/InputStepper.tsx @@ -1,4 +1,10 @@ -import React, { useState, useEffect, forwardRef } from 'react'; +import React, { + useState, + useEffect, + forwardRef, + useCallback, + memo +} from 'react'; import cn from 'classnames'; import type { ReactIconComponentType } from '@groww-tech/icon-store'; import { MdsIcRemoveMinus, MdsIcAddPlus } from '@groww-tech/icon-store/mint-icons'; @@ -13,8 +19,6 @@ export type InputStepperProps = { onChange: (value: number) => void; dataTestId?: string; width?: string; - prefixIcon?: ReactIconComponentType; - prefixLabel?: string; error?: boolean; warning?: boolean; disabled?: boolean; @@ -35,14 +39,40 @@ export type InputStepperProps = { onBlur?: (e: React.FocusEvent) => void; }; +// Memoized stepper button component +const StepperButton = memo(({ + onClick, + Icon, + disabled, + dataTestId +}: { + onClick: () => void; + Icon: ReactIconComponentType; + disabled: boolean; + dataTestId: string; +}) => ( +
+ +
+)); + +StepperButton.displayName = 'StepperButton'; + const InputStepper = forwardRef(({ placeholder, value, onChange, dataTestId, width = '128px', - prefixIcon, - prefixLabel, error = false, warning = false, disabled = false, @@ -65,65 +95,36 @@ const InputStepper = forwardRef(({ const [ isFocused, setIsFocused ] = useState(false); const [ inputValue, setInputValue ] = useState(value.toString()); - // Update inputValue when value prop changes. + // Update inputValue when value prop changes useEffect(() => { setInputValue(value.toString()); }, [ value ]); - // Focus on mount only if a parent ref is provided and it's a mutable ref object. + // Focus on mount if needed useEffect(() => { if (shouldFocusOnMount && ref && typeof ref !== 'function' && ref.current) { ref.current.focus(); } }, [ shouldFocusOnMount, ref ]); - const inputClasses = cn('inputStepper-input width100 center-align', { - contentDisabled: disabled - }); - - const inputWrapperClasses = cn('inputStepper-inputWrapper'); - - const inputContentClasses = cn( - `inputStepper-inputContent pos-rel flex ${textStyle} ${textColor} borderPrimary`, - { - [backgroundColor]: !disabled, - 'inputStepper-inputBorderNegative': error, - 'inputStepper-inputBorderWarning': warning, - 'inputStepper-inputPrefix': prefixIcon || prefixLabel, - 'inputStepper-inputFocused': isFocused && !disabled && !error, - 'backgroundSecondary contentSecondary': disabled, - contentDisabled: disabled - } - ); - - - const handleWheel = (e: React.WheelEvent) => { + // Memoized event handlers + const handleWheel = useCallback((e: React.WheelEvent) => { e.currentTarget.blur(); - }; - - - const handleCopyPaste = (e: React.ClipboardEvent) => { - if (disableCopyPaste) { - e.preventDefault(); - } - }; + }, []); - - const handleMinus = () => { + const handleMinus = useCallback(() => { if (value > min && !disabled) { onChange(value - step); } - }; - + }, [ value, min, disabled, onChange, step ]); - const handlePlus = () => { + const handlePlus = useCallback(() => { if (value < max && !disabled) { onChange(value + step); } - }; + }, [ value, max, disabled, onChange, step ]); - - const handleChange = (e: React.ChangeEvent) => { + const handleChange = useCallback((e: React.ChangeEvent) => { if (!typeable) return; const newValue = e.target.value; @@ -139,11 +140,11 @@ const InputStepper = forwardRef(({ onChange(numValue); } } - }; - + }, [ typeable, min, max, onChange ]); - const handleBlur = (e: React.FocusEvent) => { + const handleBlur = useCallback((e: React.FocusEvent) => { setIsFocused(false); + if (inputValue === '') { setInputValue('0'); onChange(0); @@ -155,35 +156,77 @@ const InputStepper = forwardRef(({ onChange(numValue); } - onBlur && onBlur(e); - - }; - + onBlur?.(e); + }, [ inputValue, onChange, onBlur ]); - const handleFocus = (e: React.FocusEvent) => { + const handleFocus = useCallback((e: React.FocusEvent) => { setIsFocused(true); - onFocus && onFocus(e); + onFocus?.(e); + }, [ onFocus ]); - }; + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + // Only block decimal point when disableDecimal is true + if (disableDecimal && e.key === '.') { + e.preventDefault(); + return; + } + + // Allow navigation/control keys + const allowedKeys = [ 'Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter', 'Home', 'End' ]; + // Block keys that aren't digits, a period, or in the allowed keys list + if (!/^[0-9]$/.test(e.key) && e.key !== '.' && !allowedKeys.includes(e.key)) { + e.preventDefault(); + return; + } - const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && onEnterPress) { onEnterPress(e); } - if (disableDecimal && e.key === '.') { + onKeyDown?.(e); + }, [ onEnterPress, disableDecimal, onKeyDown ]); + + const handleKeyUp = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'ArrowUp') { + if (value < max && !disabled) { + onChange(value + step); + } + + } else if (e.key === 'ArrowDown') { + if (value > min && !disabled) { + onChange(value - step); + } + } + + onKeyUp?.(e); + }, [ value, max, min, disabled, onChange, step, onKeyUp ]); + const handleCopyPaste = useCallback((e: React.ClipboardEvent) => { + if (disableCopyPaste) { e.preventDefault(); } + }, [ disableCopyPaste ]); + + // Memoized class names + const inputClasses = cn('inputStepper-input width100 center-align', { + contentDisabled: disabled + }); - if (onKeyDown) { - onKeyDown(e); + const inputContentClasses = cn( + `inputStepper-inputContent pos-rel flex ${textStyle} ${textColor} borderPrimary`, + { + [backgroundColor]: !disabled, + 'inputStepper-inputBorderNegative': error, + 'inputStepper-inputBorderWarning': warning, + 'inputStepper-inputFocused': isFocused && !disabled && !error, + 'backgroundSecondary contentSecondary': disabled, + contentDisabled: disabled } - }; + ); return (
@@ -191,54 +234,17 @@ const InputStepper = forwardRef(({ className={inputContentClasses} data-test-id={`${dataTestId}-content`} > -
- -
- - { - (prefixIcon || prefixLabel) && ( -
- { - prefixIcon && ( -
- {prefixIcon} -
- ) - } - { - prefixLabel && ( -
- {prefixLabel} -
- ) - } -
- ) - } + (({ onWheel={handleWheel} readOnly={!typeable} onKeyDown={handleKeyDown} - onKeyUp={onKeyUp} + onKeyUp={handleKeyUp} onCopy={handleCopyPaste} onCut={handleCopyPaste} onPaste={handleCopyPaste} /> -
- = max} - size="small" - dataTestId={`${dataTestId}-plus-button`} - /> -
+ = max} + dataTestId={`${dataTestId}-plus-container`} + />
); }); -export default InputStepper; +export default memo(InputStepper); From a5f4178b7980374db2fc0412eaac0b4120abcdd9 Mon Sep 17 00:00:00 2001 From: harshpathakzz Date: Mon, 24 Mar 2025 10:09:10 +0530 Subject: [PATCH 5/7] fixed arrow key --- .../src/components/atoms/DataRowInput/DataRowInput.tsx | 2 +- .../src/components/atoms/FreeFormInput/FreeFormInput.tsx | 4 ++++ .../src/components/atoms/InputStepper/InputStepper.tsx | 6 +++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/ui-toolkit/src/components/atoms/DataRowInput/DataRowInput.tsx b/packages/ui-toolkit/src/components/atoms/DataRowInput/DataRowInput.tsx index c647aae7..90c05478 100644 --- a/packages/ui-toolkit/src/components/atoms/DataRowInput/DataRowInput.tsx +++ b/packages/ui-toolkit/src/components/atoms/DataRowInput/DataRowInput.tsx @@ -128,7 +128,7 @@ const DataRowInput = forwardRef(({ } // Allow navigation/control keys (backspace, delete, arrows, etc.) - const allowedKeys = [ 'Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter', 'Home', 'End' ]; + const allowedKeys = [ 'Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Tab', 'Enter', 'Home', 'End' ]; // If it's not a digit, not a period, and not in the allowed keys list, block it if (!/^[0-9]$/.test(e.key) && e.key !== '.' && !allowedKeys.includes(e.key)) { diff --git a/packages/ui-toolkit/src/components/atoms/FreeFormInput/FreeFormInput.tsx b/packages/ui-toolkit/src/components/atoms/FreeFormInput/FreeFormInput.tsx index cb3d7d05..0a5e53c6 100644 --- a/packages/ui-toolkit/src/components/atoms/FreeFormInput/FreeFormInput.tsx +++ b/packages/ui-toolkit/src/components/atoms/FreeFormInput/FreeFormInput.tsx @@ -292,6 +292,10 @@ const FreeFormInput = forwardRef(({ return; } + if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { + e.preventDefault(); + } + // For number variant, only prevent non-numeric input with exceptions for navigation keys if (variant === 'number') { const allowedKeys = [ 'Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter', 'Home', 'End', '.' ]; diff --git a/packages/ui-toolkit/src/components/atoms/InputStepper/InputStepper.tsx b/packages/ui-toolkit/src/components/atoms/InputStepper/InputStepper.tsx index f352ac50..979347b9 100644 --- a/packages/ui-toolkit/src/components/atoms/InputStepper/InputStepper.tsx +++ b/packages/ui-toolkit/src/components/atoms/InputStepper/InputStepper.tsx @@ -171,8 +171,12 @@ const InputStepper = forwardRef(({ return; } + if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { + e.preventDefault(); + } + // Allow navigation/control keys - const allowedKeys = [ 'Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter', 'Home', 'End' ]; + const allowedKeys = [ 'Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Tab', 'Enter', 'Home', 'End' ]; // Block keys that aren't digits, a period, or in the allowed keys list if (!/^[0-9]$/.test(e.key) && e.key !== '.' && !allowedKeys.includes(e.key)) { From 6796b766125ee84e531fb786f4bff0976554b0fb Mon Sep 17 00:00:00 2001 From: harshpathakzz Date: Wed, 26 Mar 2025 13:30:06 +0530 Subject: [PATCH 6/7] fixed typo --- .../components/atoms/FreeFormInput/FreeFormInput.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/ui-toolkit/src/components/atoms/FreeFormInput/FreeFormInput.tsx b/packages/ui-toolkit/src/components/atoms/FreeFormInput/FreeFormInput.tsx index 0a5e53c6..c48ce2b0 100644 --- a/packages/ui-toolkit/src/components/atoms/FreeFormInput/FreeFormInput.tsx +++ b/packages/ui-toolkit/src/components/atoms/FreeFormInput/FreeFormInput.tsx @@ -22,7 +22,7 @@ type PrefixSectionProps = { PrefixIcon?: ReactIconComponentType; prefixLabel?: string; prefixIconColor: ContentMintTokens; - perfixTextColor: ContentMintTokens; + prefixTextColor: ContentMintTokens; prefixTextStyle: 'bodyBase' | 'bodyBaseHeavy'; dataTestId?: string; }; @@ -31,7 +31,7 @@ const PrefixSection = memo(({ PrefixIcon, prefixLabel, prefixIconColor, - perfixTextColor, + prefixTextColor, prefixTextStyle, dataTestId }) => { @@ -53,7 +53,7 @@ const PrefixSection = memo(({ { prefixLabel && (
{prefixLabel} @@ -196,7 +196,7 @@ export type FreeFormInputProps = { onKeyDown?: (e: React.KeyboardEvent) => void; autoComplete?: string; onKeyUp?: (e: React.KeyboardEvent) => void; - perfixTextColor?: ContentMintTokens; + prefixTextColor?: ContentMintTokens; prefixTextStyle?: 'bodyBase' | 'bodyBaseHeavy'; onEnterPress?: (e: React.KeyboardEvent) => void; disableCopyPaste?: boolean; @@ -233,7 +233,7 @@ const FreeFormInput = forwardRef(({ onKeyDown, autoComplete, onKeyUp, - perfixTextColor = 'contentSecondary', + prefixTextColor = 'contentSecondary', prefixTextStyle = 'bodyBase', onEnterPress, disableCopyPaste = false, @@ -352,7 +352,7 @@ const FreeFormInput = forwardRef(({ PrefixIcon={PrefixIcon} prefixLabel={prefixLabel} prefixIconColor={prefixIconColor} - perfixTextColor={perfixTextColor} + prefixTextColor={prefixTextColor} prefixTextStyle={prefixTextStyle} dataTestId={dataTestId} /> From 53bfb67c7cbed632e2324c0a2a658c56610ba004 Mon Sep 17 00:00:00 2001 From: harshpathakzz Date: Thu, 27 Mar 2025 12:02:41 +0530 Subject: [PATCH 7/7] moved const array --- .../src/components/atoms/DataRowInput/DataRowInput.tsx | 6 ++++-- .../src/components/atoms/FreeFormInput/FreeFormInput.tsx | 3 ++- .../src/components/atoms/InputStepper/InputStepper.tsx | 5 +++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/ui-toolkit/src/components/atoms/DataRowInput/DataRowInput.tsx b/packages/ui-toolkit/src/components/atoms/DataRowInput/DataRowInput.tsx index 90c05478..e58e4772 100644 --- a/packages/ui-toolkit/src/components/atoms/DataRowInput/DataRowInput.tsx +++ b/packages/ui-toolkit/src/components/atoms/DataRowInput/DataRowInput.tsx @@ -11,6 +11,10 @@ import { BackgroundMintTokens } from '../../../types/mint-token-types/background import { BorderMintTokens } from '../../../types/mint-token-types/border-mint-tokens'; import './styles/index.css'; +// Allow navigation/control keys (backspace, delete, arrows, etc.) +const allowedKeys = [ 'Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Tab', 'Enter', 'Home', 'End' ]; + + export type DataRowInputProps = { placeholder?: string; value: string; @@ -127,8 +131,6 @@ const DataRowInput = forwardRef(({ return; } - // Allow navigation/control keys (backspace, delete, arrows, etc.) - const allowedKeys = [ 'Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Tab', 'Enter', 'Home', 'End' ]; // If it's not a digit, not a period, and not in the allowed keys list, block it if (!/^[0-9]$/.test(e.key) && e.key !== '.' && !allowedKeys.includes(e.key)) { diff --git a/packages/ui-toolkit/src/components/atoms/FreeFormInput/FreeFormInput.tsx b/packages/ui-toolkit/src/components/atoms/FreeFormInput/FreeFormInput.tsx index c48ce2b0..75361ea4 100644 --- a/packages/ui-toolkit/src/components/atoms/FreeFormInput/FreeFormInput.tsx +++ b/packages/ui-toolkit/src/components/atoms/FreeFormInput/FreeFormInput.tsx @@ -17,6 +17,8 @@ import type { ReactIconComponentType } from '@groww-tech/icon-store'; import { ContentMintTokens } from '../../../types/mint-token-types/content-mint-tokens'; import './styles/index.css'; +const allowedKeys = [ 'Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter', 'Home', 'End', '.' ]; + type PrefixSectionProps = { PrefixIcon?: ReactIconComponentType; @@ -298,7 +300,6 @@ const FreeFormInput = forwardRef(({ // For number variant, only prevent non-numeric input with exceptions for navigation keys if (variant === 'number') { - const allowedKeys = [ 'Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter', 'Home', 'End', '.' ]; if (!/^[0-9]$/.test(e.key) && !allowedKeys.includes(e.key)) { e.preventDefault(); diff --git a/packages/ui-toolkit/src/components/atoms/InputStepper/InputStepper.tsx b/packages/ui-toolkit/src/components/atoms/InputStepper/InputStepper.tsx index 979347b9..f0cc9e54 100644 --- a/packages/ui-toolkit/src/components/atoms/InputStepper/InputStepper.tsx +++ b/packages/ui-toolkit/src/components/atoms/InputStepper/InputStepper.tsx @@ -13,6 +13,9 @@ import { ContentMintTokens } from '../../../types/mint-token-types/content-mint- import { BackgroundMintTokens } from '../../../types/mint-token-types/background-mint-tokens'; import './styles/index.css'; + // Allow navigation/control keys +const allowedKeys = [ 'Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Tab', 'Enter', 'Home', 'End' ]; + export type InputStepperProps = { placeholder?: string; value: number; @@ -175,8 +178,6 @@ const InputStepper = forwardRef(({ e.preventDefault(); } - // Allow navigation/control keys - const allowedKeys = [ 'Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Tab', 'Enter', 'Home', 'End' ]; // Block keys that aren't digits, a period, or in the allowed keys list if (!/^[0-9]$/.test(e.key) && e.key !== '.' && !allowedKeys.includes(e.key)) {