Skip to content

Commit

Permalink
Merge pull request #262 from nginformatica/feat/pin-input
Browse files Browse the repository at this point in the history
0.25.3 - Create Pin Input component
  • Loading branch information
henriquemod authored May 6, 2022
2 parents e9665d5 + 925a46b commit 597705b
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 1 deletion.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "flipper-ui",
"version": "0.25.2",
"version": "0.25.3",
"description": "",
"main": "dist/index.js",
"homepage": "https://flipper-ui.vercel.app",
Expand Down
140 changes: 140 additions & 0 deletions src/core/PinInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import React, { useEffect, useRef } from 'react'
import { TextField } from '@material-ui/core'

interface PinInputGridProps {
pin: Array<number | undefined>
setPin: (pin: Array<number | undefined>) => void
onPinChanged: (pinEntry: number | undefined, index: number) => void
pinLength: number
validationResult: boolean | undefined
isValidating: boolean
size: 'small' | 'large'
}

const removeValuesFromArray = (valuesArray: string[], value: string) => {
const valueIndex = valuesArray.findIndex(entry => entry === value)
if (valueIndex === -1) {
return
}
valuesArray.splice(valueIndex, 1)
}

const PIN_MIN_VALUE = 0
const PIN_MAX_VALUE = 9
const BACKSPACE_KEY = 'Backspace'

const PinInput: React.FC<PinInputGridProps> = ({
pinLength,
pin,
setPin,
onPinChanged,
validationResult,
isValidating,
size
}) => {
const inputRefs = useRef<HTMLInputElement[]>([])

const changePinFocus = (pinIndex: number) => {
const ref = inputRefs.current[pinIndex]
if (ref) {
ref.focus()
}
}

useEffect(() => {
changePinFocus(0)
}, [isValidating])

const onPaste = (event: React.ClipboardEvent<HTMLInputElement>) => {
const pastedValue = event.clipboardData.getData('text/plain')
const splitValues = pastedValue.split('')
const intValues: number[] = []
for (const value of splitValues) {
intValues.push(parseInt(value))
}
setPin(intValues)
changePinFocus(intValues.length - 1)
}

const onChange = (
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
index: number
) => {
if (event.target.value.length > 1) {return}
const previousValue = event.target.defaultValue
const valuesArray = event.target.value.split('')
removeValuesFromArray(valuesArray, previousValue)
const value = valuesArray.pop()
if (!value) {
return
}
const pinNumber = Number(value.trim())
if (isNaN(pinNumber) || value.length === 0) {
return
}

if (pinNumber >= PIN_MIN_VALUE && pinNumber <= PIN_MAX_VALUE) {
onPinChanged(pinNumber, index)
if (index < pinLength - 1) {
changePinFocus(index + 1)
}
}
}

const onKeyDown = (
event: React.KeyboardEvent<HTMLDivElement>,
index: number
) => {
const keyboardKeyCode = event.nativeEvent.code
if (keyboardKeyCode !== BACKSPACE_KEY) {
return
}

if (pin[index] === undefined) {
changePinFocus(index - 1)
} else {
onPinChanged(undefined, index)
}
}

return (
<>
<div>
{ Array.from({ length: pinLength }, (_, index) => (
<TextField
disabled={ isValidating }
variant='outlined'
onKeyDown={ event => onKeyDown(event, index) }
onPaste={ onPaste }
color='primary'
error={ validationResult }
style={ {
width: size === 'small' ? '40px' : '40px',
height: size === 'small' ? '30px' : '40px',
marginInline: size === 'small' ? '5px' : '10px'
} }
InputProps={ {
style: {
width: size === 'small' ? '40px' : '45px',
textAlign: 'center',
fontWeight: 'bold',
fontSize: size === 'small' ? '16px' : '20px',
padding: 'auto'
}
} }
inputRef={ (el: HTMLInputElement) => {
inputRefs.current[index] = el
} }
key={ index }
onChange={ event => {
onChange(event, index)
} }
value={ pin[index] || '' }
/>
)) }
</div>
</>
)
}

export default PinInput
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,5 @@ export { default as Tooltip } from './core/Tooltip'
export { default as Tree } from './core/Tree'
export { default as Typography } from './core/Typography'
export { default as Zoom } from './core/Zoom'
export { default as PinInput } from './core/PinInput'

114 changes: 114 additions & 0 deletions src/stories/PinInput.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React, { useState } from 'react'
import { ComponentMeta } from '@storybook/react'
import PinInput from '../core/PinInput'
import { Button } from '@material-ui/core'

export default {
title: 'PinInput',
component: PinInput
} as ComponentMeta<typeof PinInput>

const PIN_LENGTH = 6

export const Default = () => {
const [pin, setPin] = useState<Array<number | undefined>>(
new Array(PIN_LENGTH)
)

const onPinChanged = (pinEntry: number | undefined, index: number) => {
const newPin = [...pin]
newPin[index] = pinEntry
setPin(newPin)
}
const [hasError, setHasError] = useState(false)
const [isValidating, setIsValidating] = useState(false)

const handleValidation = () => {
setIsValidating(true)
if (pin.join('') !== '123123') {
setHasError(true)
setPin(new Array(PIN_LENGTH))
} else {
setHasError(false)
alert('PIN is correct')
}
setIsValidating(false)
}

return (
<>
<span>Valid pin: 123123</span>
<br />
<br />
<PinInput
pin={ pin }
setPin={ setPin }
onPinChanged={ onPinChanged }
pinLength={ PIN_LENGTH }
validationResult={ hasError }
isValidating={ isValidating }
size='small'
/>
<br />
<br />
<Button
onClick={ () => {
handleValidation()
} }>
Validate
</Button>
</>
)
}

export const Large = () => {
const [pin, setPin] = useState<Array<number | undefined>>(
new Array(PIN_LENGTH)
)

const onPinChanged = (pinEntry: number | undefined, index: number) => {
const newPin = [...pin]
newPin[index] = pinEntry
setPin(newPin)
}
const [hasError, setHasError] = useState(false)
const [isValidating, setIsValidating] = useState(false)

const handleValidation = () => {
setIsValidating(true)
if (pin.join('') !== '123123') {
setPin(new Array(PIN_LENGTH))
setHasError(true)
} else {
setHasError(false)
alert('PIN is correct')

}
setIsValidating(false)
}

return (
<>
<span>Valid pin: 123123</span>
<br />
<br />
<PinInput
pin={ pin }
setPin={ setPin }
onPinChanged={ onPinChanged }
pinLength={ PIN_LENGTH }
validationResult={ hasError }
isValidating={ isValidating }
size='large'
/>
<br />
<br />
<Button
onClick={ () => {
handleValidation()
} }>
Validate
</Button>
</>
)
}

1 comment on commit 597705b

@vercel
Copy link

@vercel vercel bot commented on 597705b May 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

flipper-ui – ./

flipper-ui-git-master-ngi.vercel.app
flipper-ui.vercel.app
flipper-ui-ngi.vercel.app

Please sign in to comment.