Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions src/components/Input/InputOtp/InputOtp.stories.ts

This file was deleted.

38 changes: 38 additions & 0 deletions src/components/Input/InputOtp/InputOtp.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useArgs } from '@storybook/preview-api'
import type { Meta, StoryObj } from '@storybook/react'

import { useCallback, useEffect, useState } from 'react'

import { InputOtp } from './InputOtp'

const meta: Meta<typeof InputOtp> = {
title: 'Form/InputOtp',
component: InputOtp,
args: { disabled: false, error: false, length: 4, value: '' },
render: (args) => {
const [, updateArgs] = useArgs()
const [value, setValue] = useState(args.value)

const onChange = useCallback(
(nextValue: string) => {
setValue(nextValue)
updateArgs({ value: nextValue })
},
[updateArgs]
)

useEffect(() => {
setValue(args.value)
}, [args.value])

return <InputOtp {...args} value={value} onChange={onChange} />
},
}

export default meta

type Story = StoryObj<typeof InputOtp>

const InputOtpStory: Story = {}

export { InputOtpStory as InputOtp }
61 changes: 37 additions & 24 deletions src/components/Input/InputOtp/InputOtp.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { memo, useCallback, useMemo, useRef, useState } from 'react'
import {
memo,
useCallback,
useImperativeHandle,
useMemo,
useRef,
useState,
type Ref,
} from 'react'

import {
Pressable,
View,
TextInput,
type TextInputProps,
type PressableProps,
type TextInputFocusEventData,
type NativeSyntheticEvent,
} from 'react-native'

import { makeStyles } from '../../../utils/makeStyles'
Expand All @@ -15,61 +25,64 @@ import { InputOtpItem } from './InputOtpItem'
export interface InputOtpProps
extends Omit<
TextInputProps,
| 'value'
| 'onChangeText'
| 'onFocus'
| 'onBlur'
| 'ref'
| 'keyboardType'
| 'style'
'onChangeText' | 'onChange' | 'ref' | 'keyboardType' | 'style'
>,
Pick<PressableProps, 'testOnly_pressed'> {
length: number
onComplete?: (value: string) => void
onChange: (value: string) => void
disabled?: boolean
error?: boolean
inputRef?: Ref<TextInput | null>
}

export const InputOtp = memo<InputOtpProps>(
({
length,
onComplete,
onChange,
disabled = false,
error = false,
testOnly_pressed,
inputRef: propsInputRef,
testID,
value = '',
onFocus,
onBlur,
...rest
}) => {
const styles = useStyles()
const [value, setValue] = useState('')
const [isFocused, setIsFocused] = useState(false)

const inputRef = useRef<TextInput>(null)

useImperativeHandle(propsInputRef, () => inputRef.current as TextInput)

const handlePress = useCallback(() => {
inputRef.current?.focus()
}, [])

const handleChange = useCallback(
(text: string) => {
const sanitizedText = text.replace(/[^0-9]/g, '')
setValue(sanitizedText)

if (sanitizedText.length === length) {
onComplete?.(sanitizedText)
inputRef.current?.blur()
}
onChange(sanitizedText)
},
[length, onComplete]
[onChange]
)

const handleFocus = useCallback(() => {
setIsFocused(true)
}, [])
const handleFocus = useCallback(
(e: NativeSyntheticEvent<TextInputFocusEventData>) => {
setIsFocused(true)
onFocus?.(e)
},
[onFocus]
)

const handleBlur = useCallback(() => {
setIsFocused(false)
}, [])
const handleBlur = useCallback(
(e: NativeSyntheticEvent<TextInputFocusEventData>) => {
setIsFocused(false)
onBlur?.(e)
},
[onBlur]
)

const activeIndex = useMemo(
() => Math.min(value.length, length - 1),
Expand Down
16 changes: 12 additions & 4 deletions src/components/Input/InputOtp/__tests__/InputOtp.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ describe('InputOtp component tests', () => {
disabled: [true, false],
error: [true, false],
testOnly_pressed: [true, false],
value: [undefined, '5', '55'],
onChange: [jest.fn()],
length: [2, 4, 8],
})

Expand All @@ -20,19 +22,25 @@ describe('InputOtp component tests', () => {
)

test('Handle input', async () => {
const mockedOnComplete = jest.fn()
const mockedOnChange = jest.fn()

const { getByTestId } = render(
<InputOtp length={4} testID='InputOtp' onComplete={mockedOnComplete} />
<InputOtp length={4} testID='InputOtp' onChange={mockedOnChange} />
)
const hiddenInput = getByTestId('InputOtpHiddenInput')

expect(mockedOnChange).not.toHaveBeenCalled()

fireEvent.changeText(hiddenInput, '55')

expect(mockedOnComplete).not.toHaveBeenCalled()
expect(mockedOnChange).toHaveBeenCalledWith('55')

fireEvent.changeText(hiddenInput, '5543')

expect(mockedOnComplete).toHaveBeenCalledWith('5543')
expect(mockedOnChange).toHaveBeenCalledWith('5543')

fireEvent.changeText(hiddenInput, '55 ')

expect(mockedOnChange).toHaveBeenCalledWith('55')
})
})
Loading
Loading