حقل الرمز (OTP Input)

حقل لإدخال الرموز التي تُستخدم مرة واحدة، لكل حرف منها خانة. ويمكن كتابة الرمز أو لصقه أو تعبئته تلقائيًا من الرسالة النصية.

التثبيت

bash
npx @smicolon/fasla-ui@latest add input-otp

قبل إضافة أي مكوّن، شغّل الأمر init مرة واحدة في مشروعك. التفاصيل في صفحة التثبيت.

معاينة

الأنماط (variant)

أربعة أنواع تختلف في طريقة تجميع الخانات.

بسيط
أرقام فقط
مع فاصل
مع تباعد

الحالات

الخانة النشطة هي التي سيُكتب فيها الحرف التالي. يُظهر الحقل حالة الخطأ عند إدخال رمز غير صحيح، ويخفت كله عند تعطيله.

الافتراضية
النشطة
التحذيرية
المعطّلة

التسمية ورسالة الخطأ

أعطِ الحقل تسمية ظاهرة، واعرض رسالة الخطأ تحته ليقرأها قارئ الشاشة مع الحقل.

رمز التحقق

أدخل الرمز المكوّن من 6 أرقام الوارد في الرسالة النصية. في هذا المثال الرمز هو 123456.

إتاحة الوصول (accessibility)

  • يقع فوق الخانات حقل input حقيقي واحد يحمل الرمز كله، فيقرؤه قارئ الشاشة حقلًا نصيًا واحدًا. ويطلب من الهاتف الرمز الوارد في الرسالة النصية بالقيمة autoComplete="one-time-code"، ويُظهر لوحة الأرقام بالقيمة inputMode="numeric". أما الخانات فمخفية عن قارئ الشاشة، حتى لا يُقرأ الرمز مرتين.
  • سمِّ الحقل دائمًا: بالسمة aria-labelledby مع تسمية ظاهرة، أو بالسمة aria-label إن لم توجد تسمية.
  • تصل السمة aria-invalid إلى الحقل نفسه، فيُعلن قارئ الشاشة أن الرمز غير صالح. واربط رسالة الخطأ بالسمة aria-describedby.
  • تمتلئ الخانات بالترتيب عند الكتابة، ويمسح مفتاح Backspace آخرها. وتنقلك مفاتيح الأسهم بين الخانات، ويملؤها اللصق كلها دفعة واحدة. ويُتجاهل كل حرف لا يطابق الخاصية pattern.
  • إذا كتبت الرمز أو لصقته بالأرقام العربية المشرقية، مثل ١٢٣، حُفظ بالأرقام اللاتينية، فيبقى الرمز واحدًا في كل اللغات ويطابق الخاصية pattern التي تقبل الأرقام وحدها.
  • يتبع الاتجاه السمة dir في الصفحة، ولا توجد له خاصية. في الصفحات من اليمين إلى اليسار تقع الخانة الأولى على اليمين ويمتلئ الرمز منها. ويتحرك كل سهم في الاتجاه الذي يشير إليه، فالسهم الأيسر ينقلك إلى الخانة التالية، في كل المتصفحات.

الخصائص (props)

InputOTP

الخاصيةالنوعالافتراضيالوصف
maxLengthnumberلا يوجدعدد حروف الرمز، ولكل حرف خانة. وهي خاصية مطلوبة.
valuestringلا يوجدالرمز في الحقل المتحكَّم به. وتُستخدم مع الدالة onChange.
defaultValuestringلا يوجدالرمز عند العرض الأول، في الحقل غير المتحكَّم به.
onChange(value) => voidلا يوجدتُستدعى بالرمز الجديد عند كل تغيير.
onComplete(value) => voidلا يوجدتُستدعى بالرمز حين تمتلئ الخانة الأخيرة.
patternstringلا يوجدتعبير نمطي يجب أن يطابقه الرمز، مثل REGEXP_ONLY_DIGITS من الحزمة input-otp. ويُتجاهل كل تغيير لا يطابقه. وتُقرأ الأرقام العربية المشرقية أرقامًا لاتينية.
pasteTransformer(text) => stringلا يوجدتنظّف النص الملصوق قبل فحصه، كأن تحذف المسافات والشرطات من رمز منسوخ.
inputModestring"numeric"لوحة المفاتيح التي تظهر على الشاشة. استخدم القيمة "text" للرموز التي فيها حروف.
autoCompletestring"one-time-code"تتيح للهاتف أن يقترح الرمز الوارد في الرسالة النصية.
disabledbooleanfalseتعطّل الحقل وتخفّفه إلى 50%.
aria-invalidbooleanلا يوجدتجعل الرمز خاطئًا: يُعلن الحقل غير صالح، ويصير حدّ كل خانة أحمر.
aria-labelstringلا يوجدالاسم الذي يقرؤه قارئ الشاشة حين لا توجد تسمية ظاهرة.
aria-labelledbystringلا يوجدمعرّف التسمية الظاهرة التي تسمّي الحقل.
aria-describedbystringلا يوجدمعرّف النص المساعد أو رسالة الخطأ التي تُقرأ مع الحقل.
namestringلا يوجدالاسم الذي يُرسل به الرمز مع النموذج.
containerClassNamestringلا يوجدأصناف إضافية للصف الذي يضم المجموعات والفواصل، مثل gap-3 لمسافة أوسع من 8px الافتراضية بينها.
childrenReactNodeلا يوجدالمجموعات والفواصل.
classNamestringلا يوجدأصناف إضافية للحقل المخفي.

InputOTPGroup

الخاصيةالنوعالافتراضيالوصف
childrenReactNodeلا يوجدالخانات، متجاورة بحدود مشتركة.
classNamestringلا يوجدأصناف إضافية للمجموعة.

InputOTPSlot

الخاصيةالنوعالافتراضيالوصف
indexnumberلا يوجدترتيب الحرف الذي تعرضه الخانة في الرمز، بدءًا من 0.
aria-invalidbooleanلا يوجدتجعل هذه الخانة وحدها خاطئة. ومن دونها تتبع الخانة السمة aria-invalid على المكوّن InputOTP.
classNamestringلا يوجدأصناف إضافية للخانة.

InputOTPSeparator

الخاصيةالنوعالافتراضيالوصف
childrenReactNodeلا يوجديحلّ محلّ الخط بمحتوى من عندك، في المربع نفسه بعرض 24px.
classNamestringلا يوجدأصناف إضافية للفاصل.

الاستخدام

حقل بسيط من ستة أرقام

tsx
import { REGEXP_ONLY_DIGITS } from "input-otp"
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSeparator,
  InputOTPSlot,
} from "@/components/ui/input-otp"

export function VerificationCode() {
  return (
    <InputOTP
      maxLength={6}
      pattern={REGEXP_ONLY_DIGITS}
      aria-label="رمز التحقق"
    >
      <InputOTPGroup>
        <InputOTPSlot index={0} />
        <InputOTPSlot index={1} />
        <InputOTPSlot index={2} />
      </InputOTPGroup>
      <InputOTPSeparator />
      <InputOTPGroup>
        <InputOTPSlot index={3} />
        <InputOTPSlot index={4} />
        <InputOTPSlot index={5} />
      </InputOTPGroup>
    </InputOTP>
  )
}

مع تسمية ورسالة خطأ

tsx
"use client"

import * as React from "react"
import { REGEXP_ONLY_DIGITS } from "input-otp"
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp"

export function VerifyCode({ onVerify }: { onVerify: (code: string) => Promise<boolean> }) {
  const labelId = React.useId()
  const messageId = React.useId()
  const [code, setCode] = React.useState("")
  const [wrong, setWrong] = React.useState(false)

  return (
    <div className="flex flex-col gap-2">
      <span id={labelId} className="text-sm font-medium">
        رمز التحقق
      </span>
      <InputOTP
        maxLength={6}
        pattern={REGEXP_ONLY_DIGITS}
        value={code}
        onChange={(next) => {
          setCode(next)
          setWrong(false)
        }}
        onComplete={async (next) => setWrong(!(await onVerify(next)))}
        aria-labelledby={labelId}
        aria-describedby={messageId}
        aria-invalid={wrong || undefined}
      >
        <InputOTPGroup>
          {Array.from({ length: 6 }, (_, index) => (
            <InputOTPSlot key={index} index={index} />
          ))}
        </InputOTPGroup>
      </InputOTP>
      <p id={messageId} className={wrong ? "text-xs text-destructive" : "text-xs text-muted-foreground"}>
        {wrong ? "الرمز غير صحيح. راجع الرسالة وحاول مرة أخرى." : "أدخل الرمز المكوّن من 6 أرقام الوارد في الرسالة النصية."}
      </p>
    </div>
  )
}

رقم سري بخانات متباعدة

tsx
import { REGEXP_ONLY_DIGITS } from "input-otp"
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp"

export function PinField() {
  return (
    <InputOTP maxLength={4} pattern={REGEXP_ONLY_DIGITS} name="pin" aria-label="الرقم السري">
      {Array.from({ length: 4 }, (_, index) => (
        <InputOTPGroup key={index}>
          <InputOTPSlot index={index} />
        </InputOTPGroup>
      ))}
    </InputOTP>
  )
}