الوحدة 1 · النموذج الذهني
اللمس والكتابة: Pressable وTextInput
19 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
اجعل Trailhead يستجيب للّمس باستخدام Pressable، ويقبل الكتابة عبر TextInput مضبوط (controlled)، بما في ذلك الاستجابة البصرية عند الضغط، وأحجام أهداف اللمس، ولوحات مفاتيح الجوّال، وتحليل ما يكتبه المستخدمون.
- 1Pressable: الخيار الافتراضي لكل ما يُنقَر
- 2أهداف اللمس وhitSlop
- 3TextInput: مضبوط، ونصّ دائمًا
- 4تحليل ما يكتبه الناس
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
زرّ الويب يحصل على تنسيقات المرور بالمؤشّر (hover) وحلقات التركيز ومؤشّر اليد مجانًا. أما على الهاتف فلا hover ولا مؤشّر: هناك إبهام، عريض وغير دقيق ومتحرّك. والتعامل الجيد مع اللمس على الجوّال يتلخّص في ثلاثة أشياء: هدف كبير بما يكفي، واستجابة بصرية لحظة ملامسة الإصبع، وحقول إدخال تفتح لوحة المفاتيح المناسبة.
Pressable: الخيار الافتراضي لكل ما يُنقَر
يغلّف Pressable أي محتوى ويُبلغ عن اللمسات. ويمكن أن يكون الـ prop المسمّى style دالة تستقبل { pressed }، فتستطيع تعتيم الزر أو تلوينه ما دام الإصبع عليه:
import { Pressable, Text } from 'react-native';
type Props = { label: string; onPress: () => void; disabled?: boolean };
export function PrimaryButton({ label, onPress, disabled = false }: Props) {
return (
<Pressable
onPress={onPress}
disabled={disabled}
accessibilityRole="button"
accessibilityState={{ disabled }}
style={({ pressed }) => ({
backgroundColor: disabled ? '#9bb5a7' : '#2f6f4e',
opacity: pressed ? 0.75 : 1,
paddingVertical: 14,
paddingHorizontal: 20,
borderRadius: 12,
alignItems: 'center',
})}
>
<Text style={{ color: 'white', fontSize: 16, fontWeight: '600' }}>{label}</Text>
</Pressable>
);
}تجعل accessibilityRole="button" قارئَي الشاشة VoiceOver وTalkBack يعلنانه زرًّا؛ ويصبح النص الذي بداخله هو تسميته. أما الأزرار التي ليس فيها إلا أيقونة، فأضف لها accessibilityLabel="Delete hike" لأنه لا يوجد نص يُقرأ.
سترى أيضًا Button وTouchableOpacity في الكود القديم. لا يمكن تنسيق Button بأكثر من لون، وعائلة Touchable* هي الواجهة الأقدم. استخدم Pressable، وابنِ component الزر الخاص بك مرة واحدة.
أهداف اللمس وhitSlop
إرشادات Apple تحدّد حدًّا أدنى 44 في 44 نقطة؛ وتقول Material Design إن الحد 48 في 48 dp. أيقونة بحجم 24 نقطة مقبولة بصريًا، لكنها أصغر من أن تُصاب وأنت تمشي على درب. يوسّع hitSlop منطقة اللمس دون تغيير التخطيط:
<Pressable
onPress={() => onDelete(hike.id)}
hitSlop={10}
accessibilityRole="button"
accessibilityLabel={`Delete ${hike.name}`}
>
<TrashIcon size={24} />
</Pressable>يعطيك Pressable أيضًا onLongPress (يُطلَق افتراضيًا بعد نحو نصف ثانية)، وonPressIn / onPressOut للّحظتين اللتين يلامس فيهما الإصبع الشاشة ثم يرتفع عنها. يستخدم Trailhead الضغط المطوّل على بطاقة الرحلة لفتح لوحة إجراءات سريعة، وهو نمط يعرفه مستخدمو iOS وAndroid من شاشاتهم الرئيسية. استخدمه للاختصارات فقط: لا أحد يكتشف الضغط المطوّل وحده، فكل إجراء خلفه يجب أن يكون متاحًا بطريقة أخرى أيضًا.
على Android يمكنك إضافة android_ripple={{ color: '#00000022' }} لتأثير التموّج الخاص بالمنصّة؛ أما iOS فيتجاهل هذا الـ prop، فيبقى تنسيق pressed مهمًّا هناك.
TextInput: مضبوط، ونصّ دائمًا
يعمل TextInput مثل <input> مضبوط، مع اختلاف واحد في التسمية. استخدم onChangeText، التي تعطيك النص الجديد. أما onChange فتعطيك كائن حدث أصليًا بدلًا من ذلك.
import { useState } from 'react';
import { Text, TextInput, View } from 'react-native';
import { PrimaryButton } from './PrimaryButton';
import { parseDistanceKm } from '../lib/parseDistance';
export function QuickLog({ onSave }: { onSave: (km: number) => void }) {
const [text, setText] = useState('');
const km = parseDistanceKm(text);
return (
<View style={{ gap: 8 }}>
<Text>Distance (km)</Text>
<TextInput
value={text}
onChangeText={setText}
accessibilityLabel="Distance (km)"
keyboardType="decimal-pad"
placeholder="8.4"
returnKeyType="done"
style={{ borderWidth: 1, borderColor: '#c9d3cc', borderRadius: 10, padding: 12, fontSize: 18 }}
/>
<PrimaryButton label="Save" disabled={km === null} onPress={() => km !== null && onSave(km)} />
</View>
);
}لاحظ ما يُخزَّن في الـ state: النص الخام text، لا رقم. لو خزّنت Number(text) بدلًا منه، لتحوّلت كتابة “8.” فورًا إلى 8 واختفت النقطة تحت إبهام المستخدم، ولأعطاك مسح الحقل 0. احتفظ بما كتبه المستخدم بالضبط، واشتقّ القيمة المحلَّلة أثناء الـ render. الزر يقرأ القيمة المشتقّة ويبقى معطّلًا حتى تصبح صالحة، وهذه استجابة أوضح من رسالة خطأ تظهر في منتصف الكتابة.
الـ props التي تشكّل لوحة مفاتيح الجوّال تستحق أن تعرفها بأسمائها:
| الـ prop | ماذا يفعل |
|---|---|
keyboardType |
decimal-pad، number-pad، email-address، phone-pad، url |
autoCapitalize |
none للبريد الإلكتروني وأسماء المستخدمين؛ والافتراضي يكبّر أول حرف في الجملة |
autoComplete / textContentType |
تسمح لنظام التشغيل باقتراح عناوين البريد وكلمات المرور ورموز التحقّق المحفوظة |
returnKeyType |
التسمية على مفتاح الإدخال: next، done، search |
secureTextEntry |
يُخفي الأحرف في كلمات المرور |
ضبط هذه الخصائص بشكل صحيح هو معظم ما يجعل النموذج يبدو أصليًا. حقل بريد إلكتروني يكبّر الحرف الأول، أو حقل رمز لا يقترح رمز التحقّق من الرسالة النصية التي وصلت للتوّ، يخبر المستخدمين أن التطبيق لم يُبنَ بعناية.
تحليل ما يكتبه الناس
هذه هي الفكرة وراء parseDistanceKm: وحّد الصيغة أولًا (احذف المسافات، حوّل إلى أحرف صغيرة، أزل “km” اللاحقة، حوّل الفاصلة إلى نقطة)، ثم اقبل فقط رقمًا موجبًا عاديًا ضمن نطاق معقول.
function parseDistanceKm(text) {
const cleaned = String(text).trim().toLowerCase().replace(/\s*km$/, '').replace(',', '.');
if (!/^\d+(\.\d+)?$/.test(cleaned)) return null;
const km = Number(cleaned);
return km > 0 && km <= 200 ? km : null;
}
for (const input of ['8.4', '8,4', ' 12 km', 'abc', '0', '1.2.3']) {
console.log(JSON.stringify(input), '->', parseDistanceKm(input));
}ولأنها دالة عادية لا تستورد شيئًا من React Native، يمكنك اختبارها في أجزاء من الثانية، وهذا ما ستفعله في القسم 5. إبقاء منطق كهذا خارج الـ components عادة تعوّض كلفتها على الجوّال، حيث يستغرق تشغيل التطبيق للتحقّق من حالة ما وقتًا أطول بكثير مما على الويب.
بهذا تكتمل الأدوات الأساسية. أما القسم 2 فعن جعل كل ذلك يبدو صحيحًا: التنسيق دون cascade، وflexbox بقيم افتراضية مختلفة، وشاشات بكل الأحجام.
أهم النقاط
Pressableهو الخيار الافتراضي لأي شيء قابل للنقر؛ ويمكن أن تكون الـstyleالخاصة به دالة تستقبل{ pressed }لتعطي استجابة بصرية فورية.- يجب ألا يقلّ هدف اللمس عن 44 في 44 نقطة؛ و
hitSlopيوسّع منطقة اللمس دون تغيير التخطيط. - الـ
TextInputالمضبوط يستخدمvalueمعonChangeText، التي تستقبل النص مباشرة. keyboardTypeيغيّر لوحة المفاتيح لا القيمة:TextInputيعطيك نصًّا دائمًا، فحلّله وتحقّق منه بنفسك.