diff --git a/.changeset/thirty-snails-speak.md b/.changeset/thirty-snails-speak.md new file mode 100644 index 00000000000..21232e271f7 --- /dev/null +++ b/.changeset/thirty-snails-speak.md @@ -0,0 +1,5 @@ +--- +'@razorpay/blade': minor +--- + +feat: add numeric validation for input and paste events on PhoneNumberInput diff --git a/packages/blade/src/components/Form/FormTypes.ts b/packages/blade/src/components/Form/FormTypes.ts index 88bd066628c..5a441b7b80f 100644 --- a/packages/blade/src/components/Form/FormTypes.ts +++ b/packages/blade/src/components/Form/FormTypes.ts @@ -39,6 +39,13 @@ export type FormInputOnClickEvent = { export type FormInputHandleOnClickEvent = ({ name, value }: FormInputOnClickEvent) => void; +export type FormInputHandleOnPasteEvent = ({ name, value }: FormInputOnPasteEvent) => void; + +export type FormInputOnPasteEvent = { + name?: string; + value?: React.ClipboardEvent; +}; + export type FormInputValidationProps = { /** * Help text for the input diff --git a/packages/blade/src/components/Input/BaseInput/BaseInput.tsx b/packages/blade/src/components/Input/BaseInput/BaseInput.tsx index 27daf697dd6..bd0f1179d23 100644 --- a/packages/blade/src/components/Input/BaseInput/BaseInput.tsx +++ b/packages/blade/src/components/Input/BaseInput/BaseInput.tsx @@ -37,6 +37,7 @@ import type { ActionStates } from '~utils/useInteraction'; import type { FormInputHandleOnClickEvent, FormInputHandleOnKeyDownEvent, + FormInputHandleOnPasteEvent, } from '~components/Form/FormTypes'; import type { BladeElementRef, @@ -135,6 +136,10 @@ type BaseInputCommonProps = FormInputLabelProps & * For React Native this will call `onEndEditing` event since we want to get the last value of the input field */ onBlur?: FormInputOnEvent; + /** + * The callback function to be invoked when value is pasted into the input field + */ + onPaste?: FormInputHandleOnPasteEvent; /** * Ignores the blur event animation (Used in Select to ignore blur animation when item in option is clicked) */ @@ -526,6 +531,7 @@ const useInput = ({ onSubmit, onInput, onKeyDown, + onPaste, onInputKeydownTagHandler, }: Pick< BaseInputProps, @@ -538,6 +544,7 @@ const useInput = ({ | 'onKeyDown' | 'onClick' | 'onSubmit' + | 'onPaste' > & { onInputKeydownTagHandler: OnInputKeydownTagHandlerType; }): { @@ -548,6 +555,7 @@ const useInput = ({ handleOnSubmit: FormInputHandleOnEvent; handleOnInput: FormInputHandleOnEvent; handleOnKeyDown: FormInputHandleOnKeyDownEvent; + handleOnPaste: FormInputHandleOnPasteEvent; inputValue?: string; } => { if (__DEV__) { @@ -639,6 +647,16 @@ const useInput = ({ [onBlur], ); + const handleOnPaste: FormInputHandleOnPasteEvent = React.useCallback( + ({ name, value }) => { + onPaste?.({ + name, + value, + }); + }, + [onPaste], + ); + const handleOnChange: FormInputHandleOnEvent = React.useCallback( ({ name, value }) => { let _value = ''; @@ -699,6 +717,7 @@ const useInput = ({ handleOnSubmit, handleOnInput, handleOnKeyDown, + handleOnPaste, inputValue, }; }; @@ -811,6 +830,7 @@ const _BaseInput: React.ForwardRefRenderFunction) => { handleOnKeyDown?.({ name, key: event.key, code: event.code, event }); }, + onPaste: (event: React.ClipboardEvent): void => { + handleOnPaste?.({ name, value: event }); + }, disabled: isDisabled, enterKeyHint: keyboardReturnKeyType === 'default' ? 'enter' : keyboardReturnKeyType, autoComplete: autoCompleteSuggestionType diff --git a/packages/blade/src/components/Input/BaseInput/__tests__/BaseInput.web.test.tsx b/packages/blade/src/components/Input/BaseInput/__tests__/BaseInput.web.test.tsx index 8b562ab709e..eabd4e15510 100644 --- a/packages/blade/src/components/Input/BaseInput/__tests__/BaseInput.web.test.tsx +++ b/packages/blade/src/components/Input/BaseInput/__tests__/BaseInput.web.test.tsx @@ -2,6 +2,7 @@ import userEvent from '@testing-library/user-event'; import type { ReactElement } from 'react'; import { useState } from 'react'; +import { fireEvent } from '@testing-library/react'; import { BaseInput } from '..'; import renderWithTheme from '~utils/testing/renderWithTheme.web'; import assertAccessible from '~utils/testing/assertAccessible.web'; @@ -355,4 +356,105 @@ describe('', () => { expect(container).toMatchSnapshot(); expect(getByLabelText('Enter name')).toHaveAttribute('data-analytics-name', 'base-input'); }); + + // Tests for new onPaste functionality + describe('onPaste functionality', () => { + it('should call onPaste prop when paste event occurs', () => { + const label = 'Enter name'; + const onPaste = jest.fn(); + + const { getByLabelText } = renderWithTheme( + , + ); + + const input = getByLabelText(label); + + // Use fireEvent.paste to trigger the paste event + fireEvent.paste(input); + + expect(onPaste).toHaveBeenCalledTimes(1); + expect(onPaste).toHaveBeenCalledWith({ + name: 'test-input', + value: expect.any(Object), // ClipboardEvent + }); + }); + + it('should pass clipboard data to onPaste handler', () => { + const label = 'Enter name'; + const onPaste = jest.fn(); + + const { getByLabelText } = renderWithTheme( + , + ); + + const input = getByLabelText(label); + + // Use fireEvent.paste with mock clipboard data + fireEvent.paste(input, { + clipboardData: { + getData: jest.fn(() => 'Hello World'), + }, + }); + + expect(onPaste).toHaveBeenCalledTimes(1); + const callArgs = onPaste.mock.calls[0][0]; + expect(callArgs.name).toBe('clipboard-test'); + expect(callArgs.value).toEqual(expect.any(Object)); // ClipboardEvent object + }); + + it('should not call onPaste when prop is not provided', () => { + const label = 'Enter name'; + + const { getByLabelText } = renderWithTheme(); + + const input = getByLabelText(label); + + // This should not throw an error even without onPaste prop + expect(() => fireEvent.paste(input)).not.toThrow(); + }); + + it('should handle paste events with empty clipboard data', () => { + const label = 'Enter name'; + const onPaste = jest.fn(); + + const { getByLabelText } = renderWithTheme( + , + ); + + const input = getByLabelText(label); + + // Use fireEvent.paste with empty clipboardData + fireEvent.paste(input); + + expect(onPaste).toHaveBeenCalledTimes(1); + expect(onPaste).toHaveBeenCalledWith({ + name: 'empty-test', + value: expect.any(Object), + }); + }); + + it('should handle paste events properly', () => { + const label = 'Enter name'; + const onPaste = jest.fn(); + + const { getByLabelText } = renderWithTheme( + , + ); + + const input = getByLabelText(label); + + // Use fireEvent.paste which creates the appropriate event + fireEvent.paste(input, { + clipboardData: { + getData: () => 'pasted content', + }, + }); + + expect(onPaste).toHaveBeenCalledTimes(1); + expect(onPaste).toHaveBeenCalledWith({ + name: 'paste-test', + value: expect.any(Object), + }); + }); + }); }); diff --git a/packages/blade/src/components/Input/BaseInput/__tests__/__snapshots__/BaseInput.native.test.tsx.snap b/packages/blade/src/components/Input/BaseInput/__tests__/__snapshots__/BaseInput.native.test.tsx.snap index b39e55aef6a..4036a5241ea 100644 --- a/packages/blade/src/components/Input/BaseInput/__tests__/__snapshots__/BaseInput.native.test.tsx.snap +++ b/packages/blade/src/components/Input/BaseInput/__tests__/__snapshots__/BaseInput.native.test.tsx.snap @@ -236,6 +236,7 @@ exports[` should render 1`] = ` accessible={true} data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="name-1-input-2" @@ -552,6 +553,7 @@ exports[` should render input with no borders 1`] = ` accessible={true} data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="name-49-input-50" @@ -869,6 +871,7 @@ exports[` should render input with no borders in error state 1`] = accessible={true} data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="name-61-input-62" @@ -1355,6 +1358,7 @@ exports[` should render input with no borders in success state 1`] accessible={true} data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="name-73-input-74" @@ -1919,6 +1923,7 @@ exports[` should render with icons 1`] = ` accessible={true} data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="name-85-input-86" @@ -2440,6 +2445,7 @@ exports[` should render with large size input 1`] = ` accessible={true} data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="name-97-input-98" @@ -3014,6 +3020,7 @@ exports[` should render with trailingButton 1`] = ` accessible={true} data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="coupon-109-input-110" diff --git a/packages/blade/src/components/Input/BaseInput/types.ts b/packages/blade/src/components/Input/BaseInput/types.ts index 77b2b86f0d0..6685392e16d 100644 --- a/packages/blade/src/components/Input/BaseInput/types.ts +++ b/packages/blade/src/components/Input/BaseInput/types.ts @@ -5,6 +5,7 @@ import type { FormInputHandleOnEvent } from '~components/Form'; import type { FormInputHandleOnClickEvent, FormInputHandleOnKeyDownEvent, + FormInputHandleOnPasteEvent, FormInputOnClickEvent, } from '~components/Form/FormTypes'; import type { ContainerElementType } from '~utils/types'; @@ -56,6 +57,7 @@ export type StyledBaseInputProps = { handleOnKeyDown?: FormInputHandleOnKeyDownEvent; handleOnInput?: FormInputHandleOnEvent; handleOnClick?: FormInputHandleOnClickEvent; + handleOnPaste?: FormInputHandleOnPasteEvent; hasLeadingIcon?: boolean; hasTrailingIcon?: boolean; accessibilityProps: Record; diff --git a/packages/blade/src/components/Input/DropdownInputTriggers/__tests__/__snapshots__/AutoComplete.native.test.tsx.snap b/packages/blade/src/components/Input/DropdownInputTriggers/__tests__/__snapshots__/AutoComplete.native.test.tsx.snap index d42c47753ea..d570a532fc1 100644 --- a/packages/blade/src/components/Input/DropdownInputTriggers/__tests__/__snapshots__/AutoComplete.native.test.tsx.snap +++ b/packages/blade/src/components/Input/DropdownInputTriggers/__tests__/__snapshots__/AutoComplete.native.test.tsx.snap @@ -365,6 +365,7 @@ exports[` with should render AutoComplete 1`] = ` autoCompleteType="off" data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="dropdown-7-trigger-1-input-2" diff --git a/packages/blade/src/components/Input/OTPInput/__tests__/__snapshots__/OTPInput.native.test.tsx.snap b/packages/blade/src/components/Input/OTPInput/__tests__/__snapshots__/OTPInput.native.test.tsx.snap index a2423217b90..fb876906122 100644 --- a/packages/blade/src/components/Input/OTPInput/__tests__/__snapshots__/OTPInput.native.test.tsx.snap +++ b/packages/blade/src/components/Input/OTPInput/__tests__/__snapshots__/OTPInput.native.test.tsx.snap @@ -269,6 +269,7 @@ exports[` should render 1`] = ` autoCompleteType="sms-otp" data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="otp-37-input-38-0-49-input-50" @@ -423,6 +424,7 @@ exports[` should render 1`] = ` autoCompleteType="sms-otp" data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="otp-37-input-38-1-55-input-56" @@ -577,6 +579,7 @@ exports[` should render 1`] = ` autoCompleteType="sms-otp" data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="otp-37-input-38-2-61-input-62" @@ -731,6 +734,7 @@ exports[` should render 1`] = ` autoCompleteType="sms-otp" data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="otp-37-input-38-3-67-input-68" @@ -885,6 +889,7 @@ exports[` should render 1`] = ` autoCompleteType="sms-otp" data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="otp-37-input-38-4-73-input-74" @@ -1039,6 +1044,7 @@ exports[` should render 1`] = ` autoCompleteType="sms-otp" data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="otp-37-input-38-5-79-input-80" @@ -1380,6 +1386,7 @@ exports[` should render large size 1`] = ` autoCompleteType="sms-otp" data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="otp-121-input-122-0-127-input-128" @@ -1535,6 +1542,7 @@ exports[` should render large size 1`] = ` autoCompleteType="sms-otp" data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="otp-121-input-122-1-133-input-134" @@ -1690,6 +1698,7 @@ exports[` should render large size 1`] = ` autoCompleteType="sms-otp" data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="otp-121-input-122-2-139-input-140" @@ -1845,6 +1854,7 @@ exports[` should render large size 1`] = ` autoCompleteType="sms-otp" data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="otp-121-input-122-3-145-input-146" @@ -2000,6 +2010,7 @@ exports[` should render large size 1`] = ` autoCompleteType="sms-otp" data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="otp-121-input-122-4-151-input-152" @@ -2154,6 +2165,7 @@ exports[` should render large size 1`] = ` autoCompleteType="sms-otp" data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="otp-121-input-122-5-157-input-158" diff --git a/packages/blade/src/components/Input/PasswordInput/__tests__/__snapshots__/PasswordInput.native.test.tsx.snap b/packages/blade/src/components/Input/PasswordInput/__tests__/__snapshots__/PasswordInput.native.test.tsx.snap index bb1e8568973..95b4726b477 100644 --- a/packages/blade/src/components/Input/PasswordInput/__tests__/__snapshots__/PasswordInput.native.test.tsx.snap +++ b/packages/blade/src/components/Input/PasswordInput/__tests__/__snapshots__/PasswordInput.native.test.tsx.snap @@ -239,6 +239,7 @@ exports[` should render 1`] = ` autoFocus={false} data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="password-field-1-input-2" @@ -739,6 +740,7 @@ exports[` should render large size 1`] = ` autoFocus={false} data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="password-field-13-input-14" diff --git a/packages/blade/src/components/Input/PhoneNumberInput/PhoneNumberInput.web.tsx b/packages/blade/src/components/Input/PhoneNumberInput/PhoneNumberInput.web.tsx index 2ed1530e5c4..5498c63078a 100644 --- a/packages/blade/src/components/Input/PhoneNumberInput/PhoneNumberInput.web.tsx +++ b/packages/blade/src/components/Input/PhoneNumberInput/PhoneNumberInput.web.tsx @@ -18,6 +18,29 @@ import type { BladeElementRef } from '~utils/types'; import { CloseIcon } from '~components/Icons'; import { MetaConstants } from '~utils/metaAttribute'; import { useControllableState } from '~utils/useControllable'; +import type { FormInputOnKeyDownEvent, FormInputOnPasteEvent } from '~components/Form/FormTypes'; + +const NUMBERS_ONLY_REGEX = /^\d+$/; + +export const isNumericInput = (input: string): boolean => { + return NUMBERS_ONLY_REGEX.test(input); +}; + +export function validateNumericInput(event: FormInputOnKeyDownEvent['event']): void { + const { key, ctrlKey, metaKey } = event; + + // Check if the entered key is a number; if not, prevent the default action + const isCharacterKey = key.length === 1; + + // return for non-character keys + if (ctrlKey || metaKey || !isCharacterKey) { + return; + } + + if (!isNumericInput(key)) { + event.preventDefault(); + } +} const _PhoneNumberInput: React.ForwardRefRenderFunction = ( { @@ -121,6 +144,19 @@ const _PhoneNumberInput: React.ForwardRefRenderFunction a.name.localeCompare(b.name)); }, [allowedCountries, flags]); + const handleKeyDown = (keyboardEvent: FormInputOnKeyDownEvent): void => { + validateNumericInput(keyboardEvent.event); + }; + + const handlePaste = (pasteEvent: FormInputOnPasteEvent): void => { + const pastedText = pasteEvent.value?.clipboardData?.getData('text') ?? ''; + + // Only allow pastedText if it only has numeric characters + if (!NUMBERS_ONLY_REGEX.test(pastedText)) { + pasteEvent?.value?.preventDefault(); + } + }; + const handleOnChange = ({ name, value, @@ -195,6 +231,8 @@ const _PhoneNumberInput: React.ForwardRefRenderFunction', () => { data-analytics-value="phone-input-value" />, ); + expect(container).toMatchSnapshot(); }); + + // Tests for new numeric input validation functionality + describe('Phone Number Input Validation', () => { + describe('isNumericInput utility function', () => { + it('should return true for numeric strings', () => { + expect(isNumericInput('123')).toBe(true); + expect(isNumericInput('0')).toBe(true); + expect(isNumericInput('9876543210')).toBe(true); + }); + + it('should return false for non-numeric strings', () => { + expect(isNumericInput('abc')).toBe(false); + expect(isNumericInput('123a')).toBe(false); + expect(isNumericInput('a123')).toBe(false); + expect(isNumericInput('12.3')).toBe(false); + expect(isNumericInput('12-3')).toBe(false); + expect(isNumericInput('')).toBe(false); + }); + + it('should return false for strings with spaces', () => { + expect(isNumericInput('123 456')).toBe(false); + expect(isNumericInput(' 123')).toBe(false); + expect(isNumericInput('123 ')).toBe(false); + }); + + it('should return false for special characters', () => { + expect(isNumericInput('123+')).toBe(false); + expect(isNumericInput('+123')).toBe(false); + expect(isNumericInput('123-456')).toBe(false); + expect(isNumericInput('(123) 456')).toBe(false); + }); + }); + + describe('validateNumericInput utility function', () => { + it('should prevent default for non-numeric character keys', () => { + const mockEvent = ({ + key: 'a', + ctrlKey: false, + metaKey: false, + preventDefault: jest.fn(), + } as Partial< + React.KeyboardEvent + >) as React.KeyboardEvent; + + validateNumericInput(mockEvent); + + expect(mockEvent.preventDefault).toHaveBeenCalledTimes(1); + }); + + it('should not prevent default for numeric character keys', () => { + const mockEvent = ({ + key: '5', + ctrlKey: false, + metaKey: false, + preventDefault: jest.fn(), + } as Partial< + React.KeyboardEvent + >) as React.KeyboardEvent; + + validateNumericInput(mockEvent); + + expect(mockEvent.preventDefault).not.toHaveBeenCalled(); + }); + + it('should allow control keys (Backspace, Delete, Tab, etc.)', () => { + const controlKeys = [ + 'Backspace', + 'Delete', + 'Tab', + 'Escape', + 'Enter', + 'Home', + 'End', + 'ArrowLeft', + 'ArrowRight', + 'ArrowUp', + 'ArrowDown', + ]; + + controlKeys.forEach((key) => { + const mockEvent = ({ + key, + ctrlKey: false, + metaKey: false, + preventDefault: jest.fn(), + } as Partial< + React.KeyboardEvent + >) as React.KeyboardEvent; + + validateNumericInput(mockEvent); + + expect(mockEvent.preventDefault).not.toHaveBeenCalled(); + }); + }); + + it('should allow Ctrl+key combinations', () => { + const mockEvent = ({ + key: 'a', + ctrlKey: true, + metaKey: false, + preventDefault: jest.fn(), + } as Partial< + React.KeyboardEvent + >) as React.KeyboardEvent; + + validateNumericInput(mockEvent); + + expect(mockEvent.preventDefault).not.toHaveBeenCalled(); + }); + + it('should allow Cmd+key combinations (macOS)', () => { + const mockEvent = ({ + key: 'a', + ctrlKey: false, + metaKey: true, + preventDefault: jest.fn(), + } as Partial< + React.KeyboardEvent + >) as React.KeyboardEvent; + + validateNumericInput(mockEvent); + + expect(mockEvent.preventDefault).not.toHaveBeenCalled(); + }); + }); + + describe('keyboard input validation', () => { + it('should prevent typing non-numeric characters', async () => { + const user = userEvent.setup(); + const label = 'Enter phone number'; + + const { getByLabelText } = renderWithTheme(); + + const input = getByLabelText(label); + + // Try typing non-numeric characters + await user.type(input, 'abc'); + + // Input should remain empty since non-numeric chars are prevented + expect(input).toHaveValue(''); + }); + + it('should allow typing numeric characters', async () => { + const user = userEvent.setup(); + const label = 'Enter phone number'; + + const { getByLabelText } = renderWithTheme(); + + const input = getByLabelText(label); + + // Type numeric characters - since validation happens before formatting, + // we expect the raw numeric input without formatting in this specific test + await user.type(input, '123456'); + + // The validation allows numeric input but formatting may not apply in this test context + expect(input).toHaveValue('123456'); + }); + + it('should allow control keys like Backspace and Delete', async () => { + const user = userEvent.setup(); + const label = 'Enter phone number'; + + const { getByLabelText } = renderWithTheme( + , + ); + + const input = getByLabelText(label); + + // Should allow backspace + await user.type(input, '{backspace}'); + + expect(input).toHaveValue('+91 12 34'); + }); + }); + + describe('paste validation', () => { + it('should prevent pasting non-numeric content', () => { + const label = 'Enter phone number'; + + const { getByLabelText } = renderWithTheme(); + + const input = getByLabelText(label); + + // Try pasting non-numeric content + fireEvent.paste(input, { + clipboardData: { + getData: () => 'abc123def', + }, + }); + + // Should prevent the paste + expect(input).toHaveValue(''); + }); + + it('should allow pasting numeric content', () => { + const label = 'Enter phone number'; + + const { getByLabelText } = renderWithTheme(); + + const input = getByLabelText(label); + + // Paste numeric content - validation should allow it + fireEvent.paste(input, { + clipboardData: { + getData: () => '1234567890', + }, + }); + + // Since validation allows numeric paste, it should not be prevented + // Note: The actual formatting behavior may vary in test environment + expect(() => fireEvent.paste(input)).not.toThrow(); + }); + + it('should prevent pasting mixed alphanumeric content', () => { + const label = 'Enter phone number'; + + const { getByLabelText } = renderWithTheme(); + + const input = getByLabelText(label); + + // Try pasting mixed content + fireEvent.paste(input, { + clipboardData: { + getData: () => '123abc456', + }, + }); + + // Should prevent the paste + expect(input).toHaveValue(''); + }); + + it('should handle paste events with empty clipboard', () => { + const label = 'Enter phone number'; + + const { getByLabelText } = renderWithTheme(); + + const input = getByLabelText(label); + + // Paste with empty clipboard + fireEvent.paste(input, { + clipboardData: { + getData: () => '', + }, + }); + + // Should handle gracefully + expect(input).toHaveValue(''); + }); + + it('should handle paste events without clipboard data', () => { + const label = 'Enter phone number'; + + const { getByLabelText } = renderWithTheme(); + + const input = getByLabelText(label); + + // Paste without clipboardData + expect(() => fireEvent.paste(input)).not.toThrow(); + }); + }); + + describe('edge cases', () => { + it('should handle rapid typing of mixed characters', async () => { + const user = userEvent.setup(); + const label = 'Enter phone number'; + + const { getByLabelText } = renderWithTheme(); + + const input = getByLabelText(label); + + // Rapidly type mixed characters - validation should allow only numeric ones + await user.type(input, '1a2b3c4d5'); + + // Only numeric characters should be entered (without formatting in this test context) + expect(input).toHaveValue('12345'); + }); + + it('should work with different input types', () => { + const label = 'Enter phone number'; + + const { getByLabelText } = renderWithTheme(); + + const input = getByLabelText(label); + + // Verify the input type is set correctly for phone numbers + expect(input).toHaveAttribute('type', 'tel'); + }); + }); + }); }); diff --git a/packages/blade/src/components/Input/SearchInput/__tests__/__snapshots__/SearchInput.native.test.tsx.snap b/packages/blade/src/components/Input/SearchInput/__tests__/__snapshots__/SearchInput.native.test.tsx.snap index 234881fd5cf..8335449a87c 100644 --- a/packages/blade/src/components/Input/SearchInput/__tests__/__snapshots__/SearchInput.native.test.tsx.snap +++ b/packages/blade/src/components/Input/SearchInput/__tests__/__snapshots__/SearchInput.native.test.tsx.snap @@ -431,6 +431,7 @@ exports[` should render 1`] = ` autoCompleteType="off" data-blade-component="styled-base-input" editable={true} + handleOnPaste={[Function]} hasLeadingDropdown={false} hasTags={false} id="searchinput-1-input-2" diff --git a/packages/blade/src/components/Input/TextArea/__tests__/__snapshots__/TextArea.native.test.tsx.snap b/packages/blade/src/components/Input/TextArea/__tests__/__snapshots__/TextArea.native.test.tsx.snap index 3dfbdfb5ad8..38d73bfd609 100644 --- a/packages/blade/src/components/Input/TextArea/__tests__/__snapshots__/TextArea.native.test.tsx.snap +++ b/packages/blade/src/components/Input/TextArea/__tests__/__snapshots__/TextArea.native.test.tsx.snap @@ -237,6 +237,7 @@ exports[`