-
-
Notifications
You must be signed in to change notification settings - Fork 32.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[base-ui][Input] Add OTP input demo #40539
Merged
Merged
Changes from 12 commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
7b7c1f9
add demo
sai6855 919930a
Update width of OTPInput component
sai6855 6176f7c
Refactor OTPInput component to remove CustomNumberInput
sai6855 2c64947
Fix arrow key navigation in OTPInput component
sai6855 af79960
Refactor OTPInput component to accept separator and inputCount as props
sai6855 20cb6ca
Refactor OTPInput component styling
sai6855 095dbd1
Add OTP Input demo to Input component
sai6855 539657d
Refactor OTPInput component to use a reusable OTP component
sai6855 ab1a664
Update OTPInput.js and OTPInput.tsx to add aria-label attribute
sai6855 7d759fb
prettier
sai6855 b91d99e
Tiny visual tweak
zanivan 6c2a8a5
More tweaks
zanivan da2757e
Update OTPInput.tsx
sai6855 581f41b
Update OTPInput.js
sai6855 82fb39f
prettier
sai6855 51965ca
move otp state to parent
sai6855 d67da7c
Update aria-label for OTP input field
sai6855 23ee497
fix selction
sai6855 b8b382f
fill inputs on paste
sai6855 bb2f3a8
handle arrow up, down
sai6855 6f58502
Add support for Delete key in OTPInput component
sai6855 7c6a232
change prop name to value and onChange
sai6855 50c9913
display entered value
sai6855 e2c30fe
fix delete key usage
sai6855 12f808a
change inputCount to otp.length
sai6855 817bc3f
separator type
sai6855 45cd082
change otp type to string from array
sai6855 c4c61d3
handle space button
sai6855 d1676f6
fix bugs
sai6855 d4462af
refactor
sai6855 7dee93b
change inputCount to length
sai6855 0da0fb5
pnpm prettier
sai6855 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
import * as React from 'react'; | ||
import { Input as BaseInput } from '@mui/base/Input'; | ||
import { Box, styled } from '@mui/system'; | ||
|
||
function OTP({ seperator, inputCount }) { | ||
const inputRefs = React.useRef(new Array(6).fill(null)); | ||
const [otp, setOtp] = React.useState(new Array(inputCount).fill('')); | ||
|
||
const focusInput = (targetIndex) => { | ||
const targetInput = inputRefs.current[targetIndex]; | ||
targetInput.focus(); | ||
}; | ||
|
||
const selectInput = (targetIndex) => { | ||
const targetInput = inputRefs.current[targetIndex]; | ||
targetInput.select(); | ||
}; | ||
|
||
const handleKeyDown = (event, currentIndex) => { | ||
switch (event.key) { | ||
case 'ArrowLeft': | ||
if (currentIndex > 0) { | ||
event.preventDefault(); | ||
focusInput(currentIndex - 1); | ||
selectInput(currentIndex - 1); | ||
} | ||
break; | ||
case 'ArrowRight': | ||
if (currentIndex < inputCount - 1) { | ||
event.preventDefault(); | ||
focusInput(currentIndex + 1); | ||
selectInput(currentIndex + 1); | ||
} | ||
break; | ||
case 'Backspace': | ||
event.preventDefault(); | ||
if (currentIndex > 0) { | ||
focusInput(currentIndex - 1); | ||
selectInput(currentIndex - 1); | ||
} | ||
|
||
setOtp((prev) => { | ||
const otpArray = [...prev]; | ||
otpArray.splice(currentIndex, 1); | ||
otpArray.push(''); | ||
return otpArray; | ||
}); | ||
|
||
break; | ||
default: | ||
break; | ||
} | ||
}; | ||
|
||
const handleChange = (event, currentIndex) => { | ||
const value = event.target.value; | ||
let indexToEnter = 0; | ||
|
||
while (indexToEnter <= currentIndex) { | ||
if (otp[indexToEnter] && indexToEnter < currentIndex) { | ||
indexToEnter += 1; | ||
} else { | ||
break; | ||
} | ||
} | ||
setOtp((prev) => { | ||
const otpArray = [...prev]; | ||
const lastValue = value[value.length - 1]; | ||
otpArray[indexToEnter] = lastValue; | ||
return otpArray; | ||
}); | ||
if (value !== '') { | ||
if (currentIndex < inputCount - 1) { | ||
focusInput(currentIndex + 1); | ||
} | ||
} | ||
}; | ||
|
||
const handleClick = (event, currentIndex) => { | ||
selectInput(currentIndex); | ||
}; | ||
|
||
return new Array(inputCount).fill(null).map((_, index) => ( | ||
<React.Fragment key={index}> | ||
<BaseInput | ||
slots={{ | ||
input: InputElement, | ||
}} | ||
aria-label="OTP input field" | ||
slotProps={{ | ||
input: { | ||
ref: (ele) => { | ||
inputRefs.current[index] = ele; | ||
}, | ||
onKeyDown: (event) => handleKeyDown(event, index), | ||
onChange: (event) => handleChange(event, index), | ||
onClick: (event) => handleClick(event, index), | ||
value: otp[index], | ||
}, | ||
}} | ||
/> | ||
{index === inputCount - 1 ? null : seperator} | ||
</React.Fragment> | ||
)); | ||
} | ||
|
||
export default function OTPInput() { | ||
return ( | ||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}> | ||
<OTP seperator={<span>-</span>} inputCount={5} /> | ||
</Box> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we display the entered code as plain text below the OTP component? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed here 50c9913 |
||
); | ||
} | ||
|
||
const blue = { | ||
100: '#DAECFF', | ||
200: '#80BFFF', | ||
400: '#3399FF', | ||
500: '#007FFF', | ||
600: '#0072E5', | ||
700: '#0059B2', | ||
}; | ||
|
||
const grey = { | ||
50: '#F3F6F9', | ||
100: '#E5EAF2', | ||
200: '#DAE2ED', | ||
300: '#C7D0DD', | ||
400: '#B0B8C4', | ||
500: '#9DA8B7', | ||
600: '#6B7A90', | ||
700: '#434D5B', | ||
800: '#303740', | ||
900: '#1C2025', | ||
}; | ||
|
||
const InputElement = styled('input')( | ||
({ theme }) => ` | ||
width: 40px; | ||
font-family: 'IBM Plex Sans', sans-serif; | ||
font-size: 0.875rem; | ||
font-weight: 400; | ||
line-height: 1.5; | ||
padding: 8px 0px; | ||
border-radius: 8px; | ||
text-align: center; | ||
color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; | ||
background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; | ||
border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; | ||
box-shadow: 0px 2px 4px ${ | ||
theme.palette.mode === 'dark' ? 'rgba(0,0,0, 0.5)' : 'rgba(0,0,0, 0.05)' | ||
}; | ||
|
||
&:hover { | ||
border-color: ${blue[400]}; | ||
} | ||
|
||
&:focus { | ||
border-color: ${blue[400]}; | ||
box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[600] : blue[200]}; | ||
} | ||
|
||
// firefox | ||
&:focus-visible { | ||
outline: 0; | ||
} | ||
`, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
import * as React from 'react'; | ||
import { Input as BaseInput } from '@mui/base/Input'; | ||
import { Box, styled } from '@mui/system'; | ||
|
||
function OTP({ | ||
seperator, | ||
inputCount, | ||
}: { | ||
seperator: React.ReactNode; | ||
inputCount: number; | ||
}) { | ||
const inputRefs = React.useRef<HTMLInputElement[]>(new Array(6).fill(null)); | ||
const [otp, setOtp] = React.useState<string[]>(new Array(inputCount).fill('')); | ||
|
||
const focusInput = (targetIndex: number) => { | ||
const targetInput = inputRefs.current[targetIndex]; | ||
targetInput.focus(); | ||
}; | ||
|
||
const selectInput = (targetIndex: number) => { | ||
const targetInput = inputRefs.current[targetIndex]; | ||
targetInput.select(); | ||
}; | ||
|
||
const handleKeyDown = ( | ||
event: React.KeyboardEvent<HTMLInputElement>, | ||
currentIndex: number, | ||
) => { | ||
switch (event.key) { | ||
case 'ArrowLeft': | ||
if (currentIndex > 0) { | ||
event.preventDefault(); | ||
focusInput(currentIndex - 1); | ||
selectInput(currentIndex - 1); | ||
} | ||
break; | ||
case 'ArrowRight': | ||
if (currentIndex < inputCount - 1) { | ||
event.preventDefault(); | ||
focusInput(currentIndex + 1); | ||
selectInput(currentIndex + 1); | ||
} | ||
break; | ||
case 'Backspace': | ||
event.preventDefault(); | ||
if (currentIndex > 0) { | ||
focusInput(currentIndex - 1); | ||
selectInput(currentIndex - 1); | ||
} | ||
|
||
setOtp((prev) => { | ||
const otpArray = [...prev]; | ||
otpArray.splice(currentIndex, 1); | ||
otpArray.push(''); | ||
return otpArray; | ||
}); | ||
|
||
break; | ||
default: | ||
break; | ||
} | ||
}; | ||
|
||
const handleChange = ( | ||
event: React.ChangeEvent<HTMLInputElement>, | ||
currentIndex: number, | ||
) => { | ||
const value = event.target.value; | ||
let indexToEnter = 0; | ||
|
||
while (indexToEnter <= currentIndex) { | ||
if (otp[indexToEnter] && indexToEnter < currentIndex) { | ||
indexToEnter += 1; | ||
} else { | ||
break; | ||
} | ||
} | ||
setOtp((prev) => { | ||
const otpArray = [...prev]; | ||
const lastValue = value[value.length - 1]; | ||
otpArray[indexToEnter] = lastValue; | ||
return otpArray; | ||
}); | ||
if (value !== '') { | ||
if (currentIndex < inputCount - 1) { | ||
focusInput(currentIndex + 1); | ||
} | ||
} | ||
}; | ||
|
||
const handleClick = ( | ||
event: React.MouseEvent<HTMLInputElement, MouseEvent>, | ||
currentIndex: number, | ||
) => { | ||
selectInput(currentIndex); | ||
}; | ||
|
||
return new Array(inputCount).fill(null).map((_, index) => ( | ||
<React.Fragment key={index}> | ||
<BaseInput | ||
slots={{ | ||
input: InputElement, | ||
}} | ||
aria-label="OTP input field" | ||
slotProps={{ | ||
input: { | ||
ref: (ele) => { | ||
inputRefs.current[index] = ele!; | ||
}, | ||
onKeyDown: (event) => handleKeyDown(event, index), | ||
onChange: (event) => handleChange(event, index), | ||
onClick: (event) => handleClick(event, index), | ||
value: otp[index], | ||
}, | ||
}} | ||
/> | ||
{index === inputCount - 1 ? null : seperator} | ||
</React.Fragment> | ||
)); | ||
} | ||
|
||
export default function OTPInput() { | ||
return ( | ||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}> | ||
<OTP seperator={<span>-</span>} inputCount={5} /> | ||
</Box> | ||
); | ||
} | ||
|
||
const blue = { | ||
100: '#DAECFF', | ||
200: '#80BFFF', | ||
400: '#3399FF', | ||
500: '#007FFF', | ||
600: '#0072E5', | ||
700: '#0059B2', | ||
}; | ||
|
||
const grey = { | ||
50: '#F3F6F9', | ||
100: '#E5EAF2', | ||
200: '#DAE2ED', | ||
300: '#C7D0DD', | ||
400: '#B0B8C4', | ||
500: '#9DA8B7', | ||
600: '#6B7A90', | ||
700: '#434D5B', | ||
800: '#303740', | ||
900: '#1C2025', | ||
}; | ||
|
||
const InputElement = styled('input')( | ||
({ theme }) => ` | ||
width: 40px; | ||
font-family: 'IBM Plex Sans', sans-serif; | ||
font-size: 0.875rem; | ||
font-weight: 400; | ||
line-height: 1.5; | ||
padding: 8px 0px; | ||
border-radius: 8px; | ||
text-align: center; | ||
color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; | ||
background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; | ||
border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; | ||
box-shadow: 0px 2px 4px ${ | ||
theme.palette.mode === 'dark' ? 'rgba(0,0,0, 0.5)' : 'rgba(0,0,0, 0.05)' | ||
}; | ||
|
||
&:hover { | ||
border-color: ${blue[400]}; | ||
} | ||
|
||
&:focus { | ||
border-color: ${blue[400]}; | ||
box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[600] : blue[200]}; | ||
} | ||
|
||
// firefox | ||
&:focus-visible { | ||
outline: 0; | ||
} | ||
`, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<OTP seperator={<span>-</span>} inputCount={5} /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please handle ArrowUp the same way as ArrowLeft and ArrowDown the same way as ArrowRight
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both
ArrowUp
andArrowLeft
does nothing (not even deselection of input) in mentioned benchmarks . so i reflected same behaviour in bb2f3a8