diff --git a/src/components/Input/InputOtp/InputOtp.tsx b/src/components/Input/InputOtp/InputOtp.tsx index 6ea3829f..a43155c3 100644 --- a/src/components/Input/InputOtp/InputOtp.tsx +++ b/src/components/Input/InputOtp/InputOtp.tsx @@ -1,6 +1,7 @@ import { memo, useCallback, + useEffect, useImperativeHandle, useMemo, useRef, @@ -48,20 +49,34 @@ export const InputOtp = memo( value = '', onFocus, onBlur, + editable, ...rest }) => { const [isFocused, setIsFocused] = useState(false) const inputRef = useRef(null) + const isInputEditable = !disabled && editable !== false useImperativeHandle( propsInputRef, () => inputRef.current ) + useEffect(() => { + if (!isInputEditable) { + setIsFocused(false) + + if (inputRef.current?.isFocused()) { + inputRef.current.blur() + } + } + }, [isInputEditable]) + const handlePress = useCallback(() => { - inputRef.current?.focus() - }, []) + if (isInputEditable) { + inputRef.current?.focus() + } + }, [isInputEditable]) const handleChange = useCallback( (text: string) => { @@ -121,6 +136,7 @@ export const InputOtp = memo( ))} { const CURSOR_ANIMATION_DURATION = 500 +const cursorAnimationStyle = { + animationName: { from: { opacity: 1 }, to: { opacity: 0.2 } }, + animationDuration: CURSOR_ANIMATION_DURATION, + animationDirection: 'alternate', + animationIterationCount: 'infinite', + animationTimingFunction: 'ease', +} satisfies AnimatedStyle + export const InputOtpItem = memo( ({ value, error, pressed, disabled, focused, testID }) => { - const opacity = useSharedValue(1) - - useEffect(() => { - if (focused) { - opacity.value = withRepeat( - withTiming(0.2, { - duration: CURSOR_ANIMATION_DURATION, - easing: Easing.ease, - }), - -1, - true - ) - } else { - opacity.value = 1 - } - }, [focused, opacity]) - - const cursorBlinking = useAnimatedStyle(() => ({ opacity: opacity.value })) - return ( ( disabled && styles.disabled, ]} > - - {value} - {focused ? ( - + {focused ? ( + + + {value} + + | - ) : null} - + + ) : ( + + {value} + + )} ) } @@ -76,11 +68,14 @@ const styles = StyleSheet.create(({ theme, border, fonts, typography }) => ({ justifyContent: 'center', }, + textRow: { flexDirection: 'row', alignItems: 'center' }, + text: { fontSize: typography.Size['text-2xl'], fontFamily: fonts.primary, fontWeight: '400', color: theme.Form.InputText.inputTextColor, + includeFontPadding: false, }, pressed: { borderColor: theme.Form.InputText.inputHoverBorderColor }, @@ -89,5 +84,5 @@ const styles = StyleSheet.create(({ theme, border, fonts, typography }) => ({ disabled: { mixBlendMode: 'luminosity', opacity: 0.6 }, - cursor: { color: theme.Form.InputText.inputTextColor }, + cursor: { color: theme.Form.InputText.inputTextColor, marginBottom: 3 }, })) diff --git a/src/components/Input/InputOtp/__tests__/InputOtp.test.tsx b/src/components/Input/InputOtp/__tests__/InputOtp.test.tsx index 32342d9e..a1c4d4a7 100644 --- a/src/components/Input/InputOtp/__tests__/InputOtp.test.tsx +++ b/src/components/Input/InputOtp/__tests__/InputOtp.test.tsx @@ -1,4 +1,5 @@ import { fireEvent, render } from '@testing-library/react-native' +import type { TextInput } from 'react-native' import { InputOtp, type InputOtpProps } from '../InputOtp' @@ -43,4 +44,105 @@ describe('InputOtp component tests', () => { expect(mockedOnChange).toHaveBeenCalledWith('55') }) + + test('should set hidden input editable prop correctly', () => { + const mockedOnChange = jest.fn() + const { getByTestId, update } = render( + + ) + + expect(getByTestId('InputOtpHiddenInput')).toHaveProp('editable', true) + + update( + + ) + + expect(getByTestId('InputOtpHiddenInput')).toHaveProp('editable', false) + + update( + + ) + + expect(getByTestId('InputOtpHiddenInput')).toHaveProp('editable', false) + }) + + test('should blur and reset focus when input becomes disabled', () => { + const mockedOnChange = jest.fn() + let inputRef: TextInput | null = null + const handleInputRef = (ref: TextInput | null) => { + inputRef = ref + } + const { getByTestId, getByText, queryByText, update } = render( + + ) + + fireEvent(getByTestId('InputOtpHiddenInput'), 'focus') + + expect(getByText('|')).toBeOnTheScreen() + + if (!inputRef) { + throw new Error('Input ref was not set') + } + + const blur = jest.fn() + + Object.assign(inputRef, { blur, isFocused: () => true }) + + update( + + ) + + expect(blur).toHaveBeenCalledOnce() + expect(queryByText('|')).not.toBeOnTheScreen() + }) + + test('should not focus hidden input on press when input is not editable', () => { + const mockedOnChange = jest.fn() + let inputRef: TextInput | null = null + const handleInputRef = (ref: TextInput | null) => { + inputRef = ref + } + const { getByTestId } = render( + + ) + + if (!inputRef) { + throw new Error('Input ref was not set') + } + + const focus = jest.fn() + + Object.assign(inputRef, { focus }) + + fireEvent.press(getByTestId('InputOtp')) + + expect(focus).not.toHaveBeenCalled() + }) }) diff --git a/src/components/Input/InputOtp/__tests__/__snapshots__/InputOtp.test.tsx.snap b/src/components/Input/InputOtp/__tests__/__snapshots__/InputOtp.test.tsx.snap index fb05b742..c48a2c5d 100644 --- a/src/components/Input/InputOtp/__tests__/__snapshots__/InputOtp.test.tsx.snap +++ b/src/components/Input/InputOtp/__tests__/__snapshots__/InputOtp.test.tsx.snap @@ -67,6 +67,7 @@ exports[`InputOtp component tests length - 2, error - false, disabled - false, p "fontFamily": "TT Fellows", "fontSize": 21, "fontWeight": "400", + "includeFontPadding": false, } } testID="undefinedItem" @@ -98,6 +99,7 @@ exports[`InputOtp component tests length - 2, error - false, disabled - false, p "fontFamily": "TT Fellows", "fontSize": 21, "fontWeight": "400", + "includeFontPadding": false, } } testID="undefinedItem" @@ -105,6 +107,7 @@ exports[`InputOtp component tests length - 2, error - false, disabled - false, p