This commit is contained in:
Rizky 2024-12-01 12:00:31 +00:00
parent 1453a545ab
commit 42eace5e4f
2 changed files with 105 additions and 71 deletions

View File

@ -2,6 +2,8 @@ import { useLocal } from "lib/utils/use-local";
import { FC, useEffect } from "react"; import { FC, useEffect } from "react";
import { FieldLocal, FieldProp, FMLocal } from "../../typings"; import { FieldLocal, FieldProp, FMLocal } from "../../typings";
import { PropTypeInput } from "./TypeInput"; import { PropTypeInput } from "./TypeInput";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "lib/comps/ui/input-otp";
import { REGEXP_ONLY_DIGITS } from "input-otp";
export const FieldOTP: FC<{ export const FieldOTP: FC<{
digit: number; digit: number;
@ -11,81 +13,44 @@ export const FieldOTP: FC<{
arg: FieldProp; arg: FieldProp;
}> = ({ digit, fm, field }) => { }> = ({ digit, fm, field }) => {
const local = useLocal({ const local = useLocal({
otp: [] as string[], otp: "",
ref: [] as HTMLInputElement[], ref: [] as HTMLInputElement[],
}); });
if (local.otp.length === 0 && digit) {
for (let i = 0; i < digit; i++) {
local.otp.push("");
}
}
return ( return (
<div className="c-flex-1 c-flex c-justify-center c-items-center"> <div
{local.otp.map((item, idx) => ( className={cx(
<input "c-flex-1 c-flex c-justify-center c-items-center",
key={idx} css`
className={cx( height: 100px;
"c-rounded-md c-text-center", .otp-single {
css` height: 80px;
margin: 3px; width: 50px;
font-size: 3em; font-size: 20px;
padding: 0px 10px; }
width: 60px; `
height: 100px; )}
border: 1px solid #ddd; >
background: white; <InputOTP
` maxLength={4}
)} value={local.otp}
inputMode="decimal" onChange={(value) => {
pattern="[0-9]*" local.otp = value;
value={item} local.render();
ref={(ref) => { if (field.on_change) {
if (ref) local.ref[idx] = ref; field.on_change({ value, name: field.name, fm });
}} }
onPaste={(e) => { }}
e.preventDefault(); pattern={REGEXP_ONLY_DIGITS}
inputMode="decimal"
var clipboardData = >
e.clipboardData || (window as any).clipboardData; <InputOTPGroup>
var pastedData = clipboardData.getData("text"); <InputOTPSlot index={0} className="otp-single" />
for (let i = 0; i < pastedData.length; i++) { <InputOTPSlot index={1} className="otp-single" />
if (i >= local.otp.length) break; <InputOTPSlot index={2} className="otp-single" />
local.otp[i] = pastedData[i]; <InputOTPSlot index={3} className="otp-single" />
} </InputOTPGroup>
local.render(); </InputOTP>
}}
onKeyDown={async (e) => {
if (e.key === "Backspace") {
let _idx = idx;
if (local.otp[_idx].length === 0) {
_idx--;
}
local.otp[_idx] = "";
local.render();
const ref = local.ref[Math.max(0, _idx - 1)];
if (ref) {
ref.focus();
}
} else if (parseInt(e.key) || e.key === "0") {
local.otp[idx] = e.key;
local.render();
const ref = local.ref[idx + 1];
if (ref) {
ref.focus();
}
}
const otp = local.otp.join("");
fm.data[field.name] = otp;
if (otp.length === digit) {
fm.render();
}
// local.render();
}}
/>
))}
</div> </div>
); );
}; };

69
comps/ui/input-otp.tsx Executable file
View File

@ -0,0 +1,69 @@
import * as React from "react"
import { OTPInput, OTPInputContext } from "input-otp"
import { Dot } from "lucide-react"
import { cn } from "@/utils"
const InputOTP = React.forwardRef<
React.ElementRef<typeof OTPInput>,
React.ComponentPropsWithoutRef<typeof OTPInput>
>(({ className, containerClassName, ...props }, ref) => (
<OTPInput
ref={ref}
containerClassName={cn(
"flex items-center gap-2 has-[:disabled]:opacity-50",
containerClassName
)}
className={cn("disabled:c-cursor-not-allowed", className)}
{...props}
/>
))
InputOTP.displayName = "InputOTP"
const InputOTPGroup = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("c-flex c-items-center", className)} {...props} />
))
InputOTPGroup.displayName = "InputOTPGroup"
const InputOTPSlot = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> & { index: number }
>(({ index, className, ...props }, ref) => {
const inputOTPContext = React.useContext(OTPInputContext)
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
return (
<div
ref={ref}
className={cn(
"c-relative c-flex c-h-10 c-w-10 c-items-center c-justify-center c-border-y c-border-r c-border-input c-text-sm c-transition-all first:c-rounded-l-md first:c-border-l last:c-rounded-r-md",
isActive && "c-z-10 c-ring-2 c-ring-ring c-ring-offset-background",
className
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="c-pointer-events-none c-absolute c-inset-0 c-flex c-items-center c-justify-center">
<div className="c-h-4 c-w-px c-animate-caret-blink c-bg-foreground c-duration-1000" />
</div>
)}
</div>
)
})
InputOTPSlot.displayName = "InputOTPSlot"
const InputOTPSeparator = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}>
<Dot />
</div>
))
InputOTPSeparator.displayName = "InputOTPSeparator"
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }