تحسين إمكانية الوصول

في الفصل السابق، نظرنا في كيفية اكتشاف الأخطاء (بما في ذلك أخطاء 404) وعرض بديل للمستخدم. ومع ذلك، ما زلنا بحاجة إلى مناقشة قطعة أخرى من الأحجية: التحقق من صحة النموذج. دعونا نرى كيفية تنفيذ التحقق من صحة النموذج من جانب الخادم باستخدام Server Actions، وكيف يمكنك عرض أخطاء النموذج باستخدام خطاف React useActionState - مع مراعاة إمكانية الوصول!

ما هي إمكانية الوصول؟

تشير إمكانية الوصول إلى تصميم وتنفيذ تطبيقات الويب التي يمكن للجميع استخدامها، بما في ذلك ذوي الإعاقات. إنه موضوع واسع يغطي العديد من المجالات، مثل التنقل باستخدام لوحة المفاتيح، وHTML الدلالي، والصور، والألوان، ومقاطع الفيديو، وغيرها.

بينما لن نخوض في تفاصيل إمكانية الوصول في هذه الدورة، سنناقش ميزات إمكانية الوصول المتاحة في Next.js وبعض الممارسات الشائعة لجعل تطبيقاتك أكثر سهولة في الوصول.

إذا كنت ترغب في معرفة المزيد عن إمكانية الوصول، نوصي بدورة Learn Accessibility من web.dev.

استخدام ملحق ESLint لإمكانية الوصول في Next.js

يتضمن Next.js ملحق eslint-plugin-jsx-a11y في تكوين ESLint الخاص به للمساعدة في اكتشاف مشكلات إمكانية الوصول مبكرًا. على سبيل المثال، يحذر هذا الملحق إذا كان لديك صور بدون نص بديل alt، أو استخدام سمات aria-* و role بشكل غير صحيح، وغير ذلك.

اختياريًا، إذا كنت ترغب في تجربة ذلك، أضف next lint كسكريبت في ملف package.json الخاص بك:

/package.json
"scripts": {
    "build": "next build",
    "dev": "next dev",
    "start": "next start",
    "lint": "next lint"
},

ثم قم بتشغيل pnpm lint في طرفيتك:

Terminal
pnpm lint

سيرشدك هذا خلال تثبيت وتكوين ESLint لمشروعك. إذا قمت بتشغيل pnpm lint الآن، يجب أن ترى الناتج التالي:

Terminal
 No ESLint warnings or errors

ومع ذلك، ماذا سيحدث إذا كان لديك صورة بدون نص بديل alt؟ دعونا نكتشف!

انتقل إلى /app/ui/invoices/table.tsx واحذف خاصية alt من الصورة. يمكنك استخدام ميزة البحث في محررك للعثور بسرعة على <Image>:

/app/ui/invoices/table.tsx
<Image
  src={invoice.image_url}
  className="rounded-full"
  width={28}
  height={28}
  alt={`${invoice.name}'s profile picture`} // احذف هذا السطر
/>

الآن قم بتشغيل pnpm lint مرة أخرى، ويجب أن ترى التحذير التالي:

Terminal
./app/ui/invoices/table.tsx
45:25  Warning: Image elements must have an alt prop,
either with meaningful text, or an empty string for decorative images. jsx-a11y/alt-text

بينما إضافة وتكوين أداة lint ليست خطوة مطلوبة، إلا أنها يمكن أن تكون مفيدة لاكتشاف مشكلات إمكانية الوصول في عملية التطوير الخاصة بك.

تحسين إمكانية الوصول للنماذج

هناك ثلاثة أشياء نقوم بها بالفعل لتحسين إمكانية الوصول في نماذجنا:

  • HTML الدلالي: استخدام عناصر دلالية (<input>, <option>, إلخ) بدلاً من <div>. هذا يسمح لتقنيات المساعدة (AT) بالتركيز على عناصر الإدخال وتقديم معلومات سياقية مناسبة للمستخدم، مما يجعل النموذج أسهل في التنقل والفهم.
  • التسمية: تضمين <label> وسمة htmlFor يضمن أن كل حقل في النموذج له نص وصفي. هذا يحسن دعم AT من خلال توفير السياق ويعزز أيضًا سهولة الاستخدام من خلال السماح للمستخدمين بالنقر على التسمية للتركيز على حقل الإدخال المقابل.
  • حد التركيز: الحقول مصممة بشكل صحيح لإظهار حد عند التركيز عليها. هذا أمر بالغ الأهمية لإمكانية الوصول لأنه يشير بصريًا إلى العنصر النشط على الصفحة، مما يساعد مستخدمي لوحة المفاتيح وقراء الشاشة على فهم مكانهم في النموذج. يمكنك التحقق من ذلك بالضغط على tab.

توفر هذه الممارسات أساسًا جيدًا لجعل نماذجك أكثر سهولة في الوصول للعديد من المستخدمين. ومع ذلك، فهي لا تعالج التحقق من صحة النموذج و الأخطاء.

التحقق من صحة النموذج

انتقل إلى http://localhost:3000/dashboard/invoices/create، وأرسل نموذجًا فارغًا. ماذا يحدث؟

ستحصل على خطأ! هذا لأنك ترسل قيم نموذج فارغة إلى Server Action الخاص بك. يمكنك منع ذلك عن طريق التحقق من صحة النموذج على العميل أو الخادم.

التحقق من صحة النموذج من جانب العميل

هناك طريقتان يمكنك من خلالهما التحقق من صحة النماذج على العميل. أبسطها هو الاعتماد على التحقق من صحة النموذج المقدم من المتصفح عن طريق إضافة السمة required إلى عناصر <input> و <select> في نماذجك. على سبيل المثال:

/app/ui/invoices/create-form.tsx
<input
  id="amount"
  name="amount"
  type="number"
  placeholder="Enter USD amount"
  className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
  required
/>

أرسل النموذج مرة أخرى. سيعرض المتصفح تحذيرًا إذا حاولت إرسال نموذج بقيم فارغة.

هذا النهج جيد بشكل عام لأن بعض تقنيات المساعدة (AT) تدعم التحقق من صحة المتصفح.

بديل للتحقق من صحة النموذج من جانب العميل هو التحقق من صحة النموذج من جانب الخادم. دعونا نرى كيف يمكنك تنفيذ ذلك في القسم التالي. في الوقت الحالي، احذف السمات required إذا أضفتها.

التحقق من صحة البيانات من جانب الخادم (Server-Side validation)

من خلال التحقق من صحة النماذج على الخادم، يمكنك:

  • التأكد من أن البيانات في التنسيق المتوقع قبل إرسالها إلى قاعدة البيانات.
  • تقليل خطر تجاوز المستخدمين الضارين للتحقق من صحة البيانات من جانب العميل (client-side validation).
  • الحصول على مصدر واحد للحقيقة لما يعتبر بيانات صالحة.

في مكون create-form.tsx الخاص بك، قم باستيراد خطاف (hook) useActionState من react. نظرًا لأن useActionState هو خطاف (hook)، ستحتاج إلى تحويل النموذج إلى مكون عميل (Client Component) باستخدام توجيه "use client":

/app/ui/invoices/create-form.tsx
'use client';
 
// ...
import { useActionState } from 'react';

داخل مكون النموذج الخاص بك، يقوم خطاف useActionState بما يلي:

  • يأخذ وسيطين: (action, initialState).
  • يعيد قيمتين: [state, formAction] - حالة النموذج، ووظيفة يتم استدعاؤها عند إرسال النموذج.

قم بتمرير إجراء createInvoice كوسيطة لـ useActionState، وداخل سمة <form action={}>، استدعِ formAction.

/app/ui/invoices/create-form.tsx
// ...
import { useActionState } from 'react';
 
export default function Form({ customers }: { customers: CustomerField[] }) {
  const [state, formAction] = useActionState(createInvoice, initialState);
 
  return <form action={formAction}>...</form>;
}

يمكن أن يكون initialState أي شيء تحدده، في هذه الحالة، قم بإنشاء كائن بمفتاحين فارغين: message و errors، وقم باستيراد نوع State من ملف actions.ts. State غير موجود بعد، لكننا سنقوم بإنشائه بعد ذلك:

/app/ui/invoices/create-form.tsx
// ...
import { createInvoice, State } from '@/app/lib/actions';
import { useActionState } from 'react';
 
export default function Form({ customers }: { customers: CustomerField[] }) {
  const initialState: State = { message: null, errors: {} };
  const [state, formAction] = useActionState(createInvoice, initialState);
 
  return <form action={formAction}>...</form>;
}

قد يبدو هذا مربكًا في البداية، لكنه سيكون أكثر وضوحًا بمجرد تحديث إجراء الخادم. لنقم بذلك الآن.

في ملف action.ts الخاص بك، يمكنك استخدام Zod للتحقق من صحة بيانات النموذج. قم بتحديث FormSchema كما يلي:

/app/lib/actions.ts
const FormSchema = z.object({
  id: z.string(),
  customerId: z.string({
    invalid_type_error: 'الرجاء تحديد عميل.',
  }),
  amount: z.coerce
    .number()
    .gt(0, { message: 'الرجاء إدخال مبلغ أكبر من $0.' }),
  status: z.enum(['pending', 'paid'], {
    invalid_type_error: 'الرجاء تحديد حالة الفاتورة.',
  }),
  date: z.string(),
});
  • customerId - يقوم Zod بالفعل بإرجاع خطأ إذا كان حقل العميل فارغًا لأنه يتوقع نوعًا string. لكن دعنا نضيف رسالة ودية إذا لم يحدد المستخدم عميلًا.
  • amount - نظرًا لأنك تقوم بتحويل نوع المبلغ من string إلى number، فسيتم تعيينه افتراضيًا إلى الصفر إذا كانت السلسلة فارغة. دعنا نخبر Zod أننا نريد دائمًا أن يكون المبلغ أكبر من 0 باستخدام دالة .gt().
  • status - يقوم Zod بالفعل بإرجاع خطأ إذا كان حقل الحالة فارغًا لأنه يتوقع "pending" أو "paid". دعنا نضيف أيضًا رسالة ودية إذا لم يحدد المستخدم حالة.

بعد ذلك، قم بتحديث إجراء createInvoice الخاص بك لقبول وسيطين - prevState و formData:

/app/lib/actions.ts
export type State = {
  errors?: {
    customerId?: string[];
    amount?: string[];
    status?: string[];
  };
  message?: string | null;
};
 
export async function createInvoice(prevState: State, formData: FormData) {
  // ...
}
  • formData - كما كان من قبل.
  • prevState - يحتوي على الحالة الممررة من خطاف useActionState. لن تستخدمه في الإجراء في هذا المثال، لكنه خاصية مطلوبة.

ثم، قم بتغيير دالة parse() من Zod إلى safeParse():

/app/lib/actions.ts
export async function createInvoice(prevState: State, formData: FormData) {
  // التحقق من صحة حقول النموذج باستخدام Zod
  const validatedFields = CreateInvoice.safeParse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
 
  // ...
}

safeParse() ستعيد كائنًا يحتوي إما على حقل success أو error. سيساعد هذا في التعامل مع التحقق من الصحة بشكل أكثر أناقة دون الحاجة إلى وضع هذا المنطق داخل كتلة try/catch.

قبل إرسال المعلومات إلى قاعدة البيانات الخاصة بك، تحقق مما إذا كانت حقول النموذج قد تم التحقق من صحتها بشكل صحيح باستخدام شرط:

/app/lib/actions.ts
export async function createInvoice(prevState: State, formData: FormData) {
  // التحقق من صحة حقول النموذج باستخدام Zod
  const validatedFields = CreateInvoice.safeParse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
 
  // إذا فشل التحقق من صحة النموذج، قم بإرجاع الأخطاء مبكرًا. وإلا، تابع.
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
      message: 'حقول مفقودة. فشل في إنشاء الفاتورة.',
    };
  }
 
  // ...
}

إذا لم يكن validatedFields ناجحًا، نعيد الدالة مبكرًا مع رسائل الخطأ من Zod.

نصيحة: قم بطباعة validatedFields في الكونسول وأرسل نموذجًا فارغًا لترى شكله.

أخيرًا، نظرًا لأنك تتعامل مع التحقق من صحة النموذج بشكل منفصل، خارج كتلة try/catch الخاصة بك، يمكنك إرجاع رسالة محددة لأي أخطاء في قاعدة البيانات، يجب أن يبدو الكود النهائي الخاص بك كما يلي:

/app/lib/actions.ts
export async function createInvoice(prevState: State, formData: FormData) {
  // التحقق من صحة النموذج باستخدام Zod
  const validatedFields = CreateInvoice.safeParse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
 
  // إذا فشل التحقق من صحة النموذج، قم بإرجاع الأخطاء مبكرًا. وإلا، تابع.
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
      message: 'حقول مفقودة. فشل في إنشاء الفاتورة.',
    };
  }
 
  // تحضير البيانات للإدراج في قاعدة البيانات
  const { customerId, amount, status } = validatedFields.data;
  const amountInCents = amount * 100;
  const date = new Date().toISOString().split('T')[0];
 
  // إدراج البيانات في قاعدة البيانات
  try {
    await sql`
      INSERT INTO invoices (customer_id, amount, status, date)
      VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
    `;
  } catch (error) {
    // إذا حدث خطأ في قاعدة البيانات، قم بإرجاع خطأ أكثر تحديدًا.
    return {
      message: 'خطأ في قاعدة البيانات: فشل في إنشاء الفاتورة.',
    };
  }
 
  // إعادة التحقق من صحة ذاكرة التخزين المؤقت لصفحة الفواتير وإعادة توجيه المستخدم.
  revalidatePath('/dashboard/invoices');
  redirect('/dashboard/invoices');
}

رائع، الآن دعونا نعرض الأخطاء في مكون النموذج الخاص بك. عد إلى مكون create-form.tsx، يمكنك الوصول إلى الأخطاء باستخدام state للنموذج.

أضف عامل ثلاثي (ternary operator) يتحقق من كل خطأ محدد. على سبيل المثال، بعد حقل العميل، يمكنك إضافة:

/app/ui/invoices/create-form.tsx
<form action={formAction}>
  <div className="rounded-md bg-gray-50 p-4 md:p-6">
    {/* اسم العميل */}
    <div className="mb-4">
      <label htmlFor="customer" className="mb-2 block text-sm font-medium">
        اختر عميلًا
      </label>
      <div className="relative">
        <select
          id="customer"
          name="customerId"
          className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
          defaultValue=""
          aria-describedby="customer-error"
        >
          <option value="" disabled>
            حدد عميلًا
          </option>
          {customers.map((name) => (
            <option key={name.id} value={name.id}>
              {name.name}
            </option>
          ))}
        </select>
        <UserCircleIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500" />
      </div>
      <div id="customer-error" aria-live="polite" aria-atomic="true">
        {state.errors?.customerId &&
          state.errors.customerId.map((error: string) => (
            <p className="mt-2 text-sm text-red-500" key={error}>
              {error}
            </p>
          ))}
      </div>
    </div>
    // ...
  </div>
</form>

نصيحة: يمكنك طباعة state في الكونسول داخل المكون الخاص بك والتحقق مما إذا كان كل شيء متصلاً بشكل صحيح. تحقق من الكونسول في أدوات المطور حيث أن نموذجك الآن مكون عميل (Client Component).

في الكود أعلاه، تقوم أيضًا بإضافة تسميات aria التالية:

  • aria-describedby="customer-error": هذا ينشئ علاقة بين عنصر select وحاوية رسالة الخطأ. يشير إلى أن الحاوية ذات id="customer-error" تصف عنصر select. ستقرأ قارئات الشاشة هذا الوصف عندما يتفاعل المستخدم مع مربع select لإعلامهم بالأخطاء.
  • id="customer-error": هذه السمة id تحدد بشكل فريد عنصر HTML الذي يحمل رسالة الخطأ لإدخال select. هذا ضروري لإنشاء العلاقة بواسطة aria-describedby.
  • aria-live="polite": يجب أن تخبر قارئة الشاشة المستخدم بأدب عند تحديث الخطأ داخل div. عندما يتغير المحتوى (على سبيل المثال، عندما يصحح المستخدم خطأً)، ستعلن قارئة الشاشة عن هذه التغييرات، ولكن فقط عندما يكون المستخدم خاملاً حتى لا تقاطعه.

تمرين: إضافة تسميات aria

باستخدام المثال أعلاه، أضف الأخطاء إلى حقول النموذج المتبقية لديك. يجب أيضًا عرض رسالة في أسفل النموذج إذا كانت أي حقول مفقودة. يجب أن تبدو واجهة المستخدم الخاصة بك كما يلي:

نموذج إنشاء فاتورة يظهر رسائل خطأ لكل حقل.

بمجرد أن تصبح جاهزًا، قم بتشغيل pnpm lint للتحقق مما إذا كنت تستخدم تسميات aria بشكل صحيح.

إذا كنت ترغب في تحدي نفسك، خذ المعرفة التي تعلمتها في هذا الفصل وأضف التحقق من صحة النموذج إلى مكون edit-form.tsx.

سوف تحتاج إلى:

  • إضافة useActionState إلى مكون edit-form.tsx الخاص بك.
  • تعديل إجراء updateInvoice للتعامل مع أخطاء التحقق من الصحة من Zod.
  • عرض الأخطاء في المكون الخاص بك، وإضافة تسميات aria لتحسين إمكانية الوصول.

بمجرد أن تصبح جاهزًا، قم بتوسيع مقتطف الكود أدناه لرؤية الحل: