Skip to content

Commit

Permalink
Use input-otp library (#3080)
Browse files Browse the repository at this point in the history
Co-authored-by: Jonas Daniels <jonas.daniels@outlook.com>
  • Loading branch information
MananTank and jnsdls authored May 20, 2024
1 parent 9c9969e commit 7abbe03
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 137 deletions.
5 changes: 5 additions & 0 deletions .changeset/stupid-cows-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Improved OTP input using input-otp library
1 change: 1 addition & 0 deletions packages/thirdweb/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@
"abitype": "1.0.0",
"fast-text-encoding": "^1.0.6",
"fuse.js": "7.0.0",
"input-otp": "^1.2.4",
"mipd": "0.0.7",
"node-libs-browser": "2.2.1",
"uqr": "0.1.2",
Expand Down
192 changes: 80 additions & 112 deletions packages/thirdweb/src/react/web/ui/components/OTPInput.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"use client";
import styled from "@emotion/styled";
import { useEffect, useRef } from "react";
import { keyframes } from "@emotion/react";
import { OTPInput as InputOTP, type SlotProps } from "input-otp";
import { useCustomTheme } from "../design-system/CustomThemeProvider.js";
import { fontSize, media, spacing } from "../design-system/index.js";
import { StyledDiv } from "../design-system/elements.js";
import { fontSize, radius, spacing } from "../design-system/index.js";
import { Container } from "./basic.js";
import { Input } from "./formElements.js";

/**
* @internal
Expand All @@ -14,129 +14,97 @@ export function OTPInput(props: {
isInvalid?: boolean;
value: string;
setValue: (value: string) => void;
onEnter?: () => void;
onEnter: () => void;
}) {
const otp = props.value.split("");

const setOTP = (newOTP: string[]) => {
props.setValue(newOTP.join(""));
};

const inputToFocusIndex = otp.length;
const boxEls = useRef<(HTMLInputElement | null)[]>([]);

useEffect(() => {
if (boxEls.current[inputToFocusIndex]) {
requestAnimationFrame(() => {
boxEls.current[inputToFocusIndex]?.focus();
});
}
}, [inputToFocusIndex]);

return (
<Container center="x" gap="sm" flex="row">
{new Array(props.digits).fill(null).map((_, i) => {
return (
<OTPInputBox
data-error={props.isInvalid}
ref={(e) => {
boxEls.current[i] = e;
}}
// biome-ignore lint/suspicious/noArrayIndexKey: in this case the index is static and has to be the key
key={i}
value={otp[i] ?? ""}
type="text"
pattern="[a-zA-Z0-9]*"
variant="outline"
inputMode="text"
onPaste={(e) => {
const pastedData = e.clipboardData.getData("text/plain");
const newOTP = pastedData.slice(0, props.digits).split("");
setOTP(newOTP);
e.preventDefault();
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (props.onEnter) {
props.onEnter();
return;
}
}

if (e.key === "ArrowLeft") {
if (i === 0) {
return;
}

boxEls.current[i - 1]?.focus();
return;
}

if (e.key === "ArrowRight") {
if (i === props.digits - 1) {
return;
}

boxEls.current[i + 1]?.focus();
return;
}

if (e.key === "e" || e.key === ".") {
e.preventDefault();
return;
}

if (e.key === "Backspace") {
if (i === 0) {
return;
}

const newOTP = otp.slice(0, -1);
setOTP(newOTP);
}
}}
onChange={(e) => {
const value = e.target.value;

if (value.length > 1) {
setOTP(value.split(""));
return;
}

if (!/\d/.test(value) && value !== "") {
e.preventDefault();
return;
}
<OTPInputContainer>
<InputOTP
onComplete={() => {
props.onEnter();
}}
maxLength={6}
value={props.value}
render={({ slots }) => (
<Container flex="row" gap="xs" center="both">
{slots.map((slot, idx) => (
// biome-ignore lint/suspicious/noArrayIndexKey: index is the only valid key here
<Slot key={idx} {...slot} isInvalid={props.isInvalid} />
))}
</Container>
)}
onChange={(newValue) => {
props.setValue(newValue);
}}
/>
</OTPInputContainer>
);
}

const newOTP = [...otp];
const index = i > inputToFocusIndex - 1 ? inputToFocusIndex : i;
const OTPInputContainer = /* @__PURE__ */ StyledDiv({
"& input": {
maxWidth: "100%",
},
});

newOTP[index] = value;
setOTP(newOTP);
}}
/>
);
})}
</Container>
function Slot(props: SlotProps & { isInvalid?: boolean }) {
return (
<OTPInputBox data-active={props.isActive} data-error={props.isInvalid}>
{props.char !== null && <div>{props.char}</div>}
{props.hasFakeCaret && <FakeCaret />}
</OTPInputBox>
);
}

const OTPInputBox = /* @__PURE__ */ styled(Input)(() => {
const caretBlink = keyframes`
0%, 100% {
opacity: 0;
},
50% {
opacity: 1;
}
`;

const FakeCaret = StyledDiv(() => {
const theme = useCustomTheme();
return {
position: "absolute",
pointerEvents: "none",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
animation: `${caretBlink} 1s infinite`,
"&::before": {
content: "''",
display: "block",
width: "1.5px",
height: "1em",
backgroundColor: theme.colors.primaryText,
},
};
});

const OTPInputBox = /* @__PURE__ */ StyledDiv(() => {
const theme = useCustomTheme();
return {
appearance: "none",
WebkitAppearance: "none",
position: "relative",
width: "40px",
height: "40px",
textAlign: "center",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: fontSize.md,
padding: spacing.xs,
[media.mobile]: {
width: "35px",
height: "35px",
boxSizing: "border-box",
transition: "color 200ms ease",
border: `2px solid ${theme.colors.borderColor}`,
"&[data-active='true']": {
borderColor: theme.colors.accentText,
},
"&[data-verify-status='invalid']": {
color: theme.colors.danger,
color: theme.colors.primaryText,
borderRadius: radius.lg,
"&[data-error='true']": {
borderColor: theme.colors.danger,
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,6 @@ export function InAppWalletOTPLoginUI(props: {
setValue={(value) => {
setOtpInput(value);
setVerifyStatus("idle"); // reset error
verify(value);
}}
onEnter={() => {
verify(otpInput);
Expand Down
43 changes: 19 additions & 24 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 7abbe03

Please sign in to comment.