Skip to content
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 32 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
7b7c1f9
add demo
sai6855 Jan 11, 2024
919930a
Update width of OTPInput component
sai6855 Jan 11, 2024
6176f7c
Refactor OTPInput component to remove CustomNumberInput
sai6855 Jan 11, 2024
2c64947
Fix arrow key navigation in OTPInput component
sai6855 Jan 12, 2024
af79960
Refactor OTPInput component to accept separator and inputCount as props
sai6855 Jan 12, 2024
20cb6ca
Refactor OTPInput component styling
sai6855 Jan 12, 2024
095dbd1
Add OTP Input demo to Input component
sai6855 Jan 12, 2024
539657d
Refactor OTPInput component to use a reusable OTP component
sai6855 Jan 12, 2024
ab1a664
Update OTPInput.js and OTPInput.tsx to add aria-label attribute
sai6855 Jan 12, 2024
7d759fb
prettier
sai6855 Jan 12, 2024
b91d99e
Tiny visual tweak
zanivan Jan 12, 2024
6c2a8a5
More tweaks
zanivan Jan 12, 2024
da2757e
Update OTPInput.tsx
sai6855 Jan 12, 2024
581f41b
Update OTPInput.js
sai6855 Jan 12, 2024
82fb39f
prettier
sai6855 Jan 12, 2024
51965ca
move otp state to parent
sai6855 Jan 16, 2024
d67da7c
Update aria-label for OTP input field
sai6855 Jan 16, 2024
23ee497
fix selction
sai6855 Jan 16, 2024
b8b382f
fill inputs on paste
sai6855 Jan 16, 2024
bb2f3a8
handle arrow up, down
sai6855 Jan 17, 2024
6f58502
Add support for Delete key in OTPInput component
sai6855 Jan 17, 2024
7c6a232
change prop name to value and onChange
sai6855 Jan 17, 2024
50c9913
display entered value
sai6855 Jan 17, 2024
e2c30fe
fix delete key usage
sai6855 Jan 17, 2024
12f808a
change inputCount to otp.length
sai6855 Jan 17, 2024
817bc3f
separator type
sai6855 Jan 17, 2024
45cd082
change otp type to string from array
sai6855 Jan 17, 2024
c4c61d3
handle space button
sai6855 Jan 17, 2024
d1676f6
fix bugs
sai6855 Jan 17, 2024
d4462af
refactor
sai6855 Jan 17, 2024
7dee93b
change inputCount to length
sai6855 Jan 18, 2024
0da0fb5
pnpm prettier
sai6855 Jan 18, 2024
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
231 changes: 231 additions & 0 deletions docs/data/base/components/input/OTPInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { Input as BaseInput } from '@mui/base/Input';
import { Box, styled } from '@mui/system';

function OTP({ separator, length, value, onChange }) {
const inputRefs = React.useRef(new Array(length).fill(null));

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 'ArrowUp':
case 'ArrowDown':
case ' ':
event.preventDefault();
break;
case 'ArrowLeft':
Copy link
Member

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Both ArrowUp and ArrowLeft does nothing (not even deselection of input) in mentioned benchmarks . so i reflected same behaviour in bb2f3a8

event.preventDefault();
if (currentIndex > 0) {
focusInput(currentIndex - 1);
selectInput(currentIndex - 1);
}
break;
case 'ArrowRight':
event.preventDefault();
if (currentIndex < length - 1) {
focusInput(currentIndex + 1);
selectInput(currentIndex + 1);
}
break;
case 'Delete':
event.preventDefault();
onChange((prevOtp) => {
const otp =
prevOtp.slice(0, currentIndex) + prevOtp.slice(currentIndex + 1);
return otp;
});

break;
case 'Backspace':
event.preventDefault();
if (currentIndex > 0) {
focusInput(currentIndex - 1);
selectInput(currentIndex - 1);
}

onChange((prevOtp) => {
const otp =
prevOtp.slice(0, currentIndex) + prevOtp.slice(currentIndex + 1);
return otp;
});
break;

default:
break;
}
};

const handleChange = (event, currentIndex) => {
const currentValue = event.target.value;
let indexToEnter = 0;

while (indexToEnter <= currentIndex) {
if (inputRefs.current[indexToEnter].value && indexToEnter < currentIndex) {
indexToEnter += 1;
} else {
break;
}
}
onChange((prev) => {
const otpArray = prev.split('');
const lastValue = currentValue[currentValue.length - 1];
otpArray[indexToEnter] = lastValue;
return otpArray.join('');
});
if (currentValue !== '') {
if (currentIndex < length - 1) {
focusInput(currentIndex + 1);
}
}
};

const handleClick = (event, currentIndex) => {
selectInput(currentIndex);
};

const handlePaste = (event, currentIndex) => {
Copy link
Member

Choose a reason for hiding this comment

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

This works great! I'm wondering if pasting in any of the digits shouldn't replace the whole value, though (so if your value is [9][9][9][9][9], have a cursor in the third input and have 12345 in the clipboard, the component should display [1][2][3][4][5] instead of [9][9][1][2][3]), but I'm not 100% sure about that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Both mentioned benchmarks displays [9][9][1][2][3] on pasting. so i reflected same behaviour.

event.preventDefault();
const clipboardData = event.clipboardData;

// Check if there is text data in the clipboard
if (clipboardData.types.includes('text/plain')) {
let pastedText = clipboardData.getData('text/plain');
pastedText = pastedText.substring(0, length).trim();
let indexToEnter = 0;

while (indexToEnter <= currentIndex) {
if (inputRefs.current[indexToEnter].value && indexToEnter < currentIndex) {
indexToEnter += 1;
} else {
break;
}
}

const otpArray = value.split('');

for (let i = indexToEnter; i < length; i += 1) {
const lastValue = pastedText[i - indexToEnter] ?? ' ';
otpArray[i] = lastValue;
}

onChange(otpArray.join(''));
}
};

return (
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
{new Array(length).fill(null).map((_, index) => (
<React.Fragment key={index}>
<BaseInput
slots={{
input: InputElement,
}}
aria-label={`Digit ${index + 1} of OTP`}
slotProps={{
input: {
ref: (ele) => {
inputRefs.current[index] = ele;
},
onKeyDown: (event) => handleKeyDown(event, index),
onChange: (event) => handleChange(event, index),
onClick: (event) => handleClick(event, index),
onPaste: (event) => handlePaste(event, index),
value: value[index] ?? '',
},
}}
/>
{index === length - 1 ? null : separator}
</React.Fragment>
))}
</Box>
);
}

OTP.propTypes = {
length: PropTypes.number.isRequired,
onChange: PropTypes.func.isRequired,
separator: PropTypes.node,
value: PropTypes.string.isRequired,
};

export default function OTPInput() {
const [otp, setOtp] = React.useState('');

return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
}}
>
<OTP separator={<span>-</span>} value={otp} onChange={setOtp} length={5} />
<span>Entered value: {otp}</span>
</Box>
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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;
}
`,
);
Loading