سنركِّز في هذا المقال على الشمولية Accessibility (أو تترجم إلى سهولة وصول أيضًا) بما في ذلك إدارة التركيز Focus Management في React التي يمكنها تحسين قابلية الاستخدام وتقليل الارتباك لكل من مستخدمي لوحة المفاتيح فقط وقارئات الشاشة.
- المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript ومعرفة استخدام سطر الأوامر أو الطرفية.
- الهدف: تعلّم كيفية تنفيذ إمكانية وصول مستخدِمي لوحة المفاتيح في React.
مستخدمو لوحة المفاتيح
أنجزنا حتى الآن جميع الميزات التي أردنا تنفيذها، إذ أصبح بإمكان المستخدِم إضافة مهمة جديدة، وتحديد المهام، وإلغاء تحديدها، وحذف المهام، وتعديل أسماء المهام، وترشيح قائمة المهام جميعها أو المهام النشطة أو المهام المكتملة، إذ يمكن للمستخدِمين تنفيذ جميع هذه المهام باستخدام الفأرة، ولكن لا يمكن لمستخدِمي لوحة المفاتيح فقط الوصول إلى هذه الميزات بسهولة.
استكشاف مشكلة قابلية استخدام لوحة المفاتيح
انقر على حقل الإدخال الموجود أعلى التطبيق كما لو أنك تريد إضافة مهمة جديدة، إذ سترى خطًا سميكًا يمثل حدود العنصر outline حول حقل الإدخال، إذ تُعَدّ هذه الحدود المؤشر المرئي على تركيز المتصفح على هذا العنصر حاليًا، لذا اضغط على مفتاح Tab
من لوحة المفاتيح، وسترى ظهور المخطط حول زر الإضافة Add تحت حقل الإدخال، إذ يدل ذلك على انتقال تركيز المتصفح.
اضغط على مفتاح Tab
عدة مرات، وسترى مؤشر التركيز المتقطع ينتقل بين أزرار الترشيح، واستمر في الضغط على مفتاح Tab
إلى أن يصبح مؤشر التركيز حول زر التعديل Edit الأول، ثم اضغط على مفتاح Enter
، إذ سيبدّل المكوِّن <Todo />
بين القوالب كما صمّمنا، وسترى نموذجًا يتيح تعديل اسم المهمة.
ولكن قد تتساءل عن مكان وجود مؤشر التركيز حاليًا، إذ تُزال تمامًا العناصر التي كانت موجودةً سابقًا لاستبدال شيء آخر بها عندما نبدِّل بين القوالب في المكوِّن <Todo />
، وهذا يعني اختفاء العنصر الذي ركّزنا عليه سابقًا، ولا يوجد شيء نركّز عليه حاليًا، فقد يؤدي ذلك إلى إرباك مجموعة كبيرة من المستخدِمين، وخاصةً المستخدِمين الذين يعتمدون على لوحة المفاتيح أو الذين يستخدِمون قارئ الشاشة، كما يمكن تحسين تجربة مستخدِمي لوحة المفاتيح وقارئ الشاشة من خلال إدارة تركيز المتصفح بأنفسنا.
التركيز بين القوالب
إذا بدَّل المستخدِم قالب <Todo/>
من قالب العرض إلى قالب التعديل، فيجب علينا التركيز على العنصر <input>
المستخدَم لإعادة تسميته، ويجب علينا إعادة التركيز مرةً أخرى إلى زر التعديل Edit عند التبديل مرةً أخرى من قالب التعديل إلى قالب العرض.
استهداف العناصر
يجب علينا إخبار React بالعنصر الذي نريد التركيز عليه في نموذج DOM وكيفية العثور عليه، ويساعدنا في ذلك الخطّاف useRef
في React الذي ينشئ كائنًا له الخاصية current
، إذ يمكن أن تكون هذه الخاصية مرجعًا لأيّ شيء نريده، ثم يمكننا البحث عن هذا المرجع لاحقًا، وهذا مفيد للإشارة إلى عناصر DOM، لذا عدِّل تعليمة الاستيراد import
في الجزء العلوي من الملف Todo.js لتتضمن الخطّاف useRef
كما يلي:
import React, { useRef, useState } from "react";
أنشئ بعد ذلك ثابتَين جديدين بعد الخطّافات في الدالة Todo()
، إذ يجب أن يكون أحد هذين الثابتين مرجعًا لزر التعديل Edit في قالب العرض والآخر لحقل التعديل في قالب التعديل.
const editFieldRef = useRef(null); const editButtonRef = useRef(null);
تملك هذه المراجع قيمةً افتراضيةً هي null
لأنها ستكون بلا قيمة حين ربطها بالعناصر الخاصة بها من خلال إضافة الخاصية ref
لكل عنصر، وضبط قيمها على كائنات ref
المسماة بأسماء مناسبة، ويجب تعديل مربع النص <input>
في قالب التعديل كما يلي:
<input id={props.id} className="todo-text" type="text" value={newName} onChange={handleChange} ref={editFieldRef} />
يجب أن يكون زر التعديل Edit في قالب العرض كما يلي:
<button type="button" className="btn" onClick={() => setEditing(true)} ref={editButtonRef} > Edit <span className="visually-hidden">{props.name}</span> </button>
التركيز على عناصر ref باستخدام الخطاف useEffect
يمكننا استخدام عناصر ref
للغرض المقصود منها عن طريق استيراد خطّاف React آخر هو useEffect()
الذي سُمِّي بهذا الاسم لأنه يعمل بعد أن تصيِّر React مكونًا معينًا، وسيشغّل هذا الخطّاف أيّ أمور إضافية نريد إضافتها إلى عملية التصيير، والتي لا يمكننا تشغيلها ضمن جسم الدالة الرئيسية، إذ يُعَدّ الخطّاف useEffect()
مفيدًا في الوضع الحالي لأننا لا نستطيع التركيز على عنصر إلا بعد تصيير المكوِّن <Todo />
ومعرفة React مكان عناصر ref
، لذا عدِّل تعليمة الاستيراد import
في الملف Todo.js مرةً أخرى لإضافة الخطّاف useEffect
كما يلي:
import React, { useEffect, useRef, useState } from "react";
يأخذ الخطّاف useEffect()
دالةً على أساس وسيط له، إذ تُنفَّذ هذه الدالة بعد تصيير المكوِّن، لذا ضع استدعاء الخطّاف useEffect()
التالي قبل تعليمة return
مباشرةً في جسم الدالة Todo()
، ومرّر إليه دالةً تسجّل العبارة "side effect" في الطرفية:
useEffect(() => { console.log("side effect"); });
أضف تابع console.log()
آخر، لتوضيح الفرق بين عملية التصيير الرئيسية وتشغيل الشيفرة ضمن الخطّاف useEffect()
، أي ضع التعليمة التالية بعد الإضافة السابقة:
console.log("main render");
افتح الآن التطبيق في متصفحك، إذ يجب أن ترى كلتا الرسالتين في الطرفية مع تكرار كل منهما ثلاث مرات، ولاحظ ظهور عبارة التصيير الرئيسي main render أولًا، ثم عبارة "side effect" ثانيًا، على الرغم من وجود عبارة "side effect" أولًا في الشيفرة.
main render (3) Todo.js:100 side effect (3) Todo.js:98
احذف تعليمة console.log("main render")
الآن، ولننتقل إلى تطبيق إدارة التركيز.
التركيز على حقل التعديل
تذكّر أننا نريد التركيز على حقل التعديل عندما ننتقل إلى قالب التعديل، لذا عدِّل الخطّاف useEffect()
ليصبح كما يلي:
useEffect(() => { if (isEditing) { editFieldRef.current.focus(); } }, [isEditing]);
إذا كانت قيمة isEditing
هي true
، فستقرأ React قيمة مرجع editFieldRef
الحالية وتنقل تركيز المتصفح إليه، كما سنمرِّر مصفوفةً إلى الخطّاف useEffect()
على أساس وسيط ثان، إذ تُعَدّ هذه المصفوفة قائمةً من القيم التي يجب أن يعتمد عليها الخطّاف useEffect()
، فإذا ضمّنا هذه القيم، فلن يُشغَّل الخطاف useEffect()
إلا عند تغيير إحدى هذه القيم، إذ نريد تغيير التركيز عندما تتغير قيمة isEditing
فقط، لذا انتقل إلى متصفحك الآن، وسترى انتقال التركيز إلى العنصر <input>
المقابل لزر التعديل Edit عند النقر عليه.
نقل التركيز إلى زر التعديل مرة أخرى
يمكن أن يبدو نقل React للتركيز مرةً أخرى إلى زر التعديل Edit عند حفظ التعديل أو إلغائه أمرًا سهلًا للوهلة الأولى، فلنجرّب إضافة شرط إلى الخطّاف useEffect للتركيز على زر التعديل إذا كانت قيمة الحالة isEditing
هي false
مثلًا، لذا عدِّل استدعاء الخطّاف useEffect()
كما يلي:
useEffect(() => { if (isEditing) { editFieldRef.current.focus(); } else { editButtonRef.current.focus(); } }, [isEditing]);
عُد إلى متصفحك وسترى انتقال تركيزك بين تعديل العنصر <input>
وزر التعديل Edit مع بدء التعديل وإنهائه، ولكن يركِّز الزر Edit في المكوِّن <Todo />
الأخير مباشرةً على تحميل الصفحة قبل تفاعلنا مع التطبيق، وهنا يتصرّف الخطّاف useEffect()
كما صممناه تمامًا، فهو يعمل بمجرد تصيير المكوِّن، ويرى أن قيمة الحالة isEditing
هي false
، كما يركِّز على الزر Edit، وبما أنه يوجد ثلاث نسخ من المكوِّن <Todo />
، فإننا نرى التركيز على الزر Edit الأخير، إذ يجب إعادة التفكير ليتغير التركيز عندما تتغير قيمة الحالة isEditing
فقط.
إدارة التركيز الصارمة
يجب معرفة قيمة الحالة isEditing
ومتى تتغير هذه القيمة، لذا يجب أن نكون قادرين على قراءة قيمة isEditing
السابقة، ويجب أن يكون منطق العمل كما يلي باستخدام الشيفرة الوهمية التالية:
if (wasNotEditingBefore && isEditingNow) { focusOnEditField() } if (wasEditingBefore && isNotEditingNow) { focusOnEditButton() }
ناقش فريق React طرقًا للحصول على حالة المكوِّن السابقة، وقدّم مثالًا عن الخطّاف المُخصَّص الذي يمكننا استخدامه لذلك.
الصق الشيفرة التالية بالقرب من أعلى الملف Todo.js قبل الدالة Todo()
:
function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; }
سنعرِّف الآن الثابت wasEditing
بعد الخطّافات في الجزء العلوي من الدالة Todo()
، إذ نريد أن يتتبع هذا الثابت قيمة isEditing
السابقة، لذلك سنستدعي الدالة usePrevious
مع isEditing
على أساس وسيط كما يلي:
const wasEditing = usePrevious(isEditing);
يمكننا باستخدام هذا الثابت تحديث الخطّاف useEffect()
لتطبيق الشفرة الوهمية التي ناقشناها سابقًاـ لذا عدِّل هذه الشيفرة لتصبح كما يلي:
useEffect(() => {
if (!wasEditing && isEditing) {
editFieldRef.current.focus();
}
if (wasEditing && !isEditing) {
editButtonRef.current.focus();
}
}, [wasEditing, isEditing]);
لاحظ اعتماد منطق useEffect()
الآن على الثابت wasEditing
، لذلك سنقدِّمه ضمن مصفوفة الاعتماديات، وحاول مرةً أخرى استخدام زرَي التعديل Edit والإلغاء Cancel للتبديل بين قوالب المكوِّن <Todo />
، إذ سترى مؤشر تركيز المتصفح يتحرك بطريقة مناسبة دون ظهور المشكلة التي ناقشناها سابقًا.
التركيز عندما يحذف المستخدم مهمة
هناك مشكلة واحدة أخيرة في تجربة لوحة المفاتيح، وهي اختفاء التركيز عندما يحذف المستخدِم مهمةً من القائمة، لذا سنتبع نمطًا مشابهًا للتعديلات السابقة، إذ سننشئ مرجعًا جديدًا، وسنستخدِم الخطّاف usePrevious()
، لنتمكّن من التركيز على عنوان القائمة عندما يحذف المستخدِم مهمةً.
يكون المكان الذي نرغب في إرسال تركيزنا إليه واضحًا في بعض الأحيان، إذ كانت لدينا نقطة أصل للرجوع إليها وهي زر التعديل Edit عند التبديل بين قوالب <Todo />
، لكن لا يوجد مكان للرجوع إليه في حالتنا، لأننا نزيل العناصر تمامًا من نموذج DOM، فعنوان القائمة هو أفضل خيار لأنه قريب من عنصر القائمة الذي سيحذفه المستخدِم، كما سيخبر التركيز عليه المستخدِم بعدد المهام المتبقية.
إنشاء المرجع
استورد الخطّافَين useRef()
وuseEffect()
إلى الملف App.js كما يلي:
import React, { useState, useRef, useEffect } from "react";
صرّح بعد ذلك عن مرجع جديد ضمن الدالة App()
قبل تعليمة return
مباشرةً كما يلي:
const listHeadingRef = useRef(null);
تحضير العنوان
لا تكون عناصر العنوان مثل <h2>
قابلةً للتركيز عادةً، ولكن لا يُعَدّ ذلك مشكلة، إذ يمكننا جعل أيّ عنصر قابلًا للتركيز برمجيًا عن طريق إضافة السمة tabindex="-1"
إليه، أي يمكن التركيز عليه باستخدام لغة جافاسكربت فقط، كما لا يمكنك الضغط على مفتاح Tab
للتركيز على عنصر له السمة tabindex="-1"
باستخدام الطريقة نفسها التي يمكنك اتباعها مع عناصر <button>
أو <a>
، إذ يمكن تطبيق ذلك باستخدام السمة tabindex="0"
، ولكنه لا يُعَدّ مناسبًا في هذه الحالة.
لنضِف السمة tabindex
المكتوبة بالصورة tabIndex
في صيغة JSX إلى العنوان الموجود أعلى قائمة المهام مع المرجع headingRef
كما يلي:
<h2 id="list-heading" tabIndex="-1" ref={listHeadingRef}> {headingText} </h2>
ملاحظة: تُعَدّ السمة tabindex
رائعةً لحالات إمكانية الوصول الطارئة، ولكن يجب عليك الحرص على عدم الإفراط في استخدامها، إذ يمكنك تطبيقها على عنصر فقط عندما تكون متأكدًا تمامًا من أنّ جعله قابلًا للتركيز سيفيد المستخدِم بطريقة ما، كما يجب عليك استخدام العناصر التي يمكن التركيز عليها طبيعيًا مثل الأزرار وعناصر الروابط وحقول الإدخال، في حين يمكن أن يكون لاستخدام السمة tabindex
غير المسؤول تأثيرًا سلبيًا عميقًا على مستخدِمي لوحة المفاتيح وقارئات الشاشة.
الحصول على الحالة السابقة
نريد التركيز على العنصر المرتبط بالمرجع باستخدام السمة ref
عندما يحذف المستخدِم مهمةً من القائمة فقط، إذ سيتطلب ذلك الخطّاف usePrevious()
الذي استخدمناه سابقًا، لذا أضف هذا الخطّاف في أعلى الملف App.js بعد عبارات الاستيراد مباشرةً كما يلي:
function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; }
أضف الآن ما يلي قبل تعليمة return
ضمن الدالة App()
:
const prevTaskLength = usePrevious(tasks.length);
استدعينا الخطّاف usePrevious()
لتتبع طول حالة المهام.
ملاحظة: بما أننا نستخدِم الآن الخطّاف usePrevious()
في ملفين، فستتمثّل إعادة البناء الفعالة في نقل الدالة usePrevious()
إلى ملفها، وتصديرها من هذا الملف، واستيرادها حيث تريدها.
استخدام الخطاف useEffect() للتحكم في التركيز الرئيسي
يمكننا الآن إعداد الخطّاف useEffect()
لتشغيله عندما يتغير عدد المهام، والذي سيركِّز على العنوان إذا كان عدد المهام أقل مما كان عليه سابقًا، أي أننا حذفنا مهمةً.
أضف ما يلي إلى جسم الدالة App()
بعد الإضافات السابقة مباشرةً:
useEffect(() => { if (tasks.length - prevTaskLength === -1) { listHeadingRef.current.focus(); } }, [tasks.length, prevTaskLength]);
نحاول التركيز فقط على عنوان القائمة إذا كانت لدينا مهام أقل مما كانت عليه سابقًا، إذ تضمن الاعتماديات Dependencies االمُمرَّرة في هذا الخطّاف محاولة إعادة التشغيل فقط عندما تتغير أيّ من هذه القيم، أي عدد المهام الحالية أو عدد المهام السابقة، وسترى الآن ظهور مخطط التركيز المنقط حول العنوان أعلى القائمة عندما تحذف مهمةً في متصفحك.
الخلاصة
انتهينا من إنشاء تطبيق React من الألف إلى الياء، وستكون المهارات التي تعلمتها أساسًا للبناء عليها لتواصل العمل مع React، كما يمكنك أن تكون مساهمًا فعالًا في مشروع React حتى إذا كان كل ما تفعله هو التفكير مليًا في المكونات وحالتها وخاصياتها، وتذكّر أن تكتب دائمًا أفضل شيفرة HTML ممكنة.
يُعَدّ الخطّافان useRef()
وuseEffect()
ميزات متقدمةً إلى حد ما، ويجب أن تفخر بنفسك لاستخدامها، وابحث عن فرص لممارستها أكثر، مما سيسمح لك بإنشاء تجارب شاملة للمستخدِمين، إذ يمكن تعذُّر الوصول إلى تطبيقنا لمستخدِمي لوحة المفاتيح بدونهما.
ملاحظة: إذا كنت بحاجة إلى التحقق من شيفرتك مقابل نسختنا من الشيفرة، فيمكنك العثور على نسخة نهائية من نموذج شيفرة تطبيق React في المستودع todo-reaction. كما يمكنك الحصول على إصدار مباشر قيد التشغيل من خلال مراجعة todo-react-build.
سنقدِّم في المقال التالي قائمةً بموارد React التي يمكنك استخدامها للمضي قدمًا في مسار تعلّمك.
ترجمة -وبتصرُّف- للمقال Accessibility in React.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.