{"id":"l-rn-1-4","courseId":"react-native-apps","lang":"ar","kind":"lesson","summary":"اجعل Trailhead يستجيب للّمس باستخدام Pressable، ويقبل الكتابة عبر TextInput مضبوط (controlled)، بما في ذلك الاستجابة البصرية عند الضغط، وأحجام أهداف اللمس، ولوحات مفاتيح الجوّال، وتحليل ما يكتبه المستخدمون.","html":"<p>زرّ الويب يحصل على تنسيقات المرور بالمؤشّر (hover) وحلقات التركيز ومؤشّر اليد مجانًا. أما على الهاتف فلا hover ولا مؤشّر: هناك إبهام، عريض وغير دقيق ومتحرّك. والتعامل الجيد مع اللمس على الجوّال يتلخّص في ثلاثة أشياء: هدف كبير بما يكفي، واستجابة بصرية لحظة ملامسة الإصبع، وحقول إدخال تفتح لوحة المفاتيح المناسبة.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Pressable: الخيار الافتراضي لكل ما يُنقَر</h2>\n<p>يغلّف <code dir=\"ltr\">Pressable</code> أي محتوى ويُبلغ عن اللمسات. ويمكن أن يكون الـ prop المسمّى <code dir=\"ltr\">style</code> دالة تستقبل <code dir=\"ltr\">{ pressed }</code>، فتستطيع تعتيم الزر أو تلوينه ما دام الإصبع عليه:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">src/components/PrimaryButton.tsx</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { Pressable, Text } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react-native'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Props</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">label</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">onPress</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> void</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">disabled</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> boolean</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> PrimaryButton</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">label</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">onPress</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">disabled</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Props</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Pressable</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      onPress</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">onPress</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      disabled</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">disabled</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      accessibilityRole</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"button\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      accessibilityState</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ disabled }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">pressed</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        backgroundColor: disabled </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '#9bb5a7'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '#2f6f4e'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        opacity: pressed </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.75</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        paddingVertical: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">14</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        paddingHorizontal: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">20</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        borderRadius: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">12</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        alignItems: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'center'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      })</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    ></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ color: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'white'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, fontSize: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, fontWeight: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'600'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">label</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Pressable</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  );</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>تجعل <code dir=\"ltr\">accessibilityRole=&quot;button&quot;</code> قارئَي الشاشة VoiceOver وTalkBack يعلنانه زرًّا؛ ويصبح النص الذي بداخله هو تسميته. أما الأزرار التي ليس فيها إلا أيقونة، فأضف لها <code dir=\"ltr\">accessibilityLabel=&quot;Delete hike&quot;</code> لأنه لا يوجد نص يُقرأ.</p>\n<p>سترى أيضًا <code dir=\"ltr\">Button</code> و<code dir=\"ltr\">TouchableOpacity</code> في الكود القديم. لا يمكن تنسيق <code dir=\"ltr\">Button</code> بأكثر من لون، وعائلة <code dir=\"ltr\">Touchable*</code> هي الواجهة الأقدم. استخدم <code dir=\"ltr\">Pressable</code>، وابنِ component الزر الخاص بك مرة واحدة.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">أهداف اللمس وhitSlop</h2>\n<p>إرشادات Apple تحدّد حدًّا أدنى 44 في 44 نقطة؛ وتقول Material Design إن الحد 48 في 48 dp. أيقونة بحجم 24 نقطة مقبولة بصريًا، لكنها أصغر من أن تُصاب وأنت تمشي على درب. يوسّع <code dir=\"ltr\">hitSlop</code> منطقة اللمس دون تغيير التخطيط:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">TSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Pressable</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  onPress</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> onDelete</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(hike.id)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  hitSlop</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">10</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  accessibilityRole</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"button\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  accessibilityLabel</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`Delete ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">hike</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">name</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">TrashIcon</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> size</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">24</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> /></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Pressable</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<p>يعطيك <code dir=\"ltr\">Pressable</code> أيضًا <code dir=\"ltr\">onLongPress</code> (يُطلَق افتراضيًا بعد نحو نصف ثانية)، و<code dir=\"ltr\">onPressIn</code> / <code dir=\"ltr\">onPressOut</code> للّحظتين اللتين يلامس فيهما الإصبع الشاشة ثم يرتفع عنها. يستخدم Trailhead الضغط المطوّل على بطاقة الرحلة لفتح لوحة إجراءات سريعة، وهو نمط يعرفه مستخدمو iOS وAndroid من شاشاتهم الرئيسية. استخدمه للاختصارات فقط: لا أحد يكتشف الضغط المطوّل وحده، فكل إجراء خلفه يجب أن يكون متاحًا بطريقة أخرى أيضًا.</p>\n<p>على Android يمكنك إضافة <code dir=\"ltr\">android_ripple={{ color: '#00000022' }}</code> لتأثير التموّج الخاص بالمنصّة؛ أما iOS فيتجاهل هذا الـ prop، فيبقى تنسيق <code dir=\"ltr\">pressed</code> مهمًّا هناك.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">TextInput: مضبوط، ونصّ دائمًا</h2>\n<p>يعمل <code dir=\"ltr\">TextInput</code> مثل <code dir=\"ltr\">&lt;input&gt;</code> مضبوط، مع اختلاف واحد في التسمية. استخدم <code dir=\"ltr\">onChangeText</code>، التي تعطيك النص الجديد. أما <code dir=\"ltr\">onChange</code> فتعطيك كائن حدث أصليًا بدلًا من ذلك.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">src/components/QuickLog.tsx</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { useState } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { Text, TextInput, View } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react-native'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { PrimaryButton } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> './PrimaryButton'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { parseDistanceKm } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '../lib/parseDistance'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> QuickLog</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">onSave</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">onSave</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">km</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> void</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setText</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">''</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> km</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> parseDistanceKm</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(text);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">View</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ gap: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">8</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Distance (km)&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">TextInput</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">text</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        onChangeText</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">setText</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        accessibilityLabel</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Distance (km)\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        keyboardType</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"decimal-pad\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        placeholder</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"8.4\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        returnKeyType</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"done\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ borderWidth: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, borderColor: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'#c9d3cc'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, borderRadius: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">10</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, padding: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">12</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, fontSize: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">18</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      /></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">PrimaryButton</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> label</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Save\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> disabled</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">km </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onPress</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> km </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!==</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x26;&#x26;</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> onSave</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(km)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> /></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">View</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  );</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>لاحظ ما يُخزَّن في الـ state: النص الخام <code dir=\"ltr\">text</code>، لا رقم. لو خزّنت <code dir=\"ltr\">Number(text)</code> بدلًا منه، لتحوّلت كتابة “8.” فورًا إلى <code dir=\"ltr\">8</code> واختفت النقطة تحت إبهام المستخدم، ولأعطاك مسح الحقل <code dir=\"ltr\">0</code>. احتفظ بما كتبه المستخدم بالضبط، واشتقّ القيمة المحلَّلة أثناء الـ render. الزر يقرأ القيمة المشتقّة ويبقى معطّلًا حتى تصبح صالحة، وهذه استجابة أوضح من رسالة خطأ تظهر في منتصف الكتابة.</p>\n<p>الـ props التي تشكّل لوحة مفاتيح الجوّال تستحق أن تعرفها بأسمائها:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"جدول قابل للتمرير\" tabindex=\"0\"><table><thead>\n<tr>\n<th>الـ prop</th>\n<th>ماذا يفعل</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code dir=\"ltr\">keyboardType</code></td>\n<td><code dir=\"ltr\">decimal-pad</code>، <code dir=\"ltr\">number-pad</code>، <code dir=\"ltr\">email-address</code>، <code dir=\"ltr\">phone-pad</code>، <code dir=\"ltr\">url</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">autoCapitalize</code></td>\n<td><code dir=\"ltr\">none</code> للبريد الإلكتروني وأسماء المستخدمين؛ والافتراضي يكبّر أول حرف في الجملة</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">autoComplete</code> / <code dir=\"ltr\">textContentType</code></td>\n<td>تسمح لنظام التشغيل باقتراح عناوين البريد وكلمات المرور ورموز التحقّق المحفوظة</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">returnKeyType</code></td>\n<td>التسمية على مفتاح الإدخال: <code dir=\"ltr\">next</code>، <code dir=\"ltr\">done</code>، <code dir=\"ltr\">search</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">secureTextEntry</code></td>\n<td>يُخفي الأحرف في كلمات المرور</td>\n</tr>\n</tbody>\n</table></div><p>ضبط هذه الخصائص بشكل صحيح هو معظم ما يجعل النموذج يبدو أصليًا. حقل بريد إلكتروني يكبّر الحرف الأول، أو حقل رمز لا يقترح رمز التحقّق من الرسالة النصية التي وصلت للتوّ، يخبر المستخدمين أن التطبيق لم يُبنَ بعناية.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>الثقة بلوحة المفاتيح للتحقّق من الإدخال</span></p>\n<p><code dir=\"ltr\">keyboardType=&quot;decimal-pad&quot;</code> تلميح، لا مُرشِّح. المستخدمون يلصقون نصوصًا، ولوحات المفاتيح الخارجية تكتب أي شيء، وعلى هاتف ألماني يكون المفتاح العشري فاصلة، فتصل “8,4” نصًّا لا تستطيع <code dir=\"ltr\">Number()</code> قراءته (<code dir=\"ltr\">Number('8,4')</code> تساوي <code dir=\"ltr\">NaN</code>). حلّل كل إدخال في دالة نقية صغيرة، وتعامل مع <code dir=\"ltr\">null</code> على أنها «ليست صالحة بعد».</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">تحليل ما يكتبه الناس</h2>\n<p>هذه هي الفكرة وراء <code dir=\"ltr\">parseDistanceKm</code>: وحّد الصيغة أولًا (احذف المسافات، حوّل إلى أحرف صغيرة، أزل “km” اللاحقة، حوّل الفاصلة إلى نقطة)، ثم اقبل فقط رقمًا موجبًا عاديًا ضمن نطاق معقول.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-run=\"js\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"js\" data-index=\"3\" aria-label=\"شغّل هذا المثال\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>تشغيل</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> parseDistanceKm</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> cleaned</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> String</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(text).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">trim</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">().</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toLowerCase</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">().</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">replace</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">\\s</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">km</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">$</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">''</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">replace</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">','</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'.'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">^</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">\\d</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#116329;--shiki-light-font-weight:bold;--shiki-dark:#7EE787;--shiki-dark-font-weight:bold\">\\.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">\\d</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?$</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(cleaned)) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> km</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(cleaned);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> km </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">></span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x26;&#x26;</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> km </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x3C;=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 200</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> km </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> input</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> of</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'8.4'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'8,4'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">' 12 km'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'abc'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'0'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'1.2.3'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]) {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">JSON</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">stringify</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(input), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'->'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">parseDistanceKm</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(input));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre><div class=\"run-output\" data-run-output=\"3\" hidden></div></div>\n<p>ولأنها دالة عادية لا تستورد شيئًا من React Native، يمكنك اختبارها في أجزاء من الثانية، وهذا ما ستفعله في القسم 5. إبقاء منطق كهذا خارج الـ components عادة تعوّض كلفتها على الجوّال، حيث يستغرق تشغيل التطبيق للتحقّق من حالة ما وقتًا أطول بكثير مما على الويب.</p>\n<p>بهذا تكتمل الأدوات الأساسية. أما القسم 2 فعن جعل كل ذلك يبدو صحيحًا: التنسيق دون cascade، وflexbox بقيم افتراضية مختلفة، وشاشات بكل الأحجام.</p>\n","toc":[{"id":"sec-1","text":"Pressable: الخيار الافتراضي لكل ما يُنقَر","level":2},{"id":"sec-2","text":"أهداف اللمس وhitSlop","level":2},{"id":"sec-3","text":"TextInput: مضبوط، ونصّ دائمًا","level":2},{"id":"sec-4","text":"تحليل ما يكتبه الناس","level":2}],"takeaways":["<code dir=\"ltr\">Pressable</code> هو الخيار الافتراضي لأي شيء قابل للنقر؛ ويمكن أن تكون الـ <code dir=\"ltr\">style</code> الخاصة به دالة تستقبل <code dir=\"ltr\">{ pressed }</code> لتعطي استجابة بصرية فورية.","يجب ألا يقلّ هدف اللمس عن 44 في 44 نقطة؛ و<code dir=\"ltr\">hitSlop</code> يوسّع منطقة اللمس دون تغيير التخطيط.","الـ <code dir=\"ltr\">TextInput</code> المضبوط يستخدم <code dir=\"ltr\">value</code> مع <code dir=\"ltr\">onChangeText</code>، التي تستقبل النص مباشرة.","<code dir=\"ltr\">keyboardType</code> يغيّر لوحة المفاتيح لا القيمة: <code dir=\"ltr\">TextInput</code> يعطيك نصًّا دائمًا، فحلّله وتحقّق منه بنفسك."],"further":[{"title":"Pressable","url":"https://reactnative.dev/docs/pressable"},{"title":"TextInput","url":"https://reactnative.dev/docs/textinput"},{"title":"Handling Touches","url":"https://reactnative.dev/docs/handling-touches"}],"quiz":[{"q":"<p>وضعت <code dir=\"ltr\">keyboardType=&quot;decimal-pad&quot;</code> على حقل المسافة في Trailhead. ما نوع القيمة التي تصل إلى <code dir=\"ltr\">onChangeText</code>؟</p>","options":[{"text":"رقم، لأن لوحة المفاتيح لا تسمح إلا بالأرقام.","why":"نوع لوحة المفاتيح يغيّر فقط المفاتيح المعروضة. القيمة نصّ دائمًا، واللصق ما زال قادرًا على إدخال أي شيء."},{"text":"نصّ (string) يجب أن تحلّله؛ وفي بعض الإعدادات الإقليمية سيحوي فاصلة بدلًا من النقطة العشرية.","why":"صحيح. لوحة المفاتيح الألمانية أو الفرنسية تعرض مفتاح فاصلة، لذا فإن “8,4” شيء معقول تمامًا أن يكتبه المستخدم."},{"text":"رقم على iOS ونصّ على Android.","why":"المنصّتان تعطيانك نصًّا. الفروق بين المنصّات تظهر في المفاتيح المعروضة، لا في النوع."}],"answer":1},{"q":"<p>يقول المستخدمون إن أيقونة سلّة المهملات الصغيرة في Trailhead صعبة الإصابة، لكن التصميم يجب أن يبقى 24 في 24 نقطة. ما أفضل إصلاح؟</p>","options":[{"text":"أضف <code dir=\"ltr\">hitSlop={10}</code> إلى الـ Pressable لتكبر منطقة اللمس 10 نقاط من كل جانب.","why":"صحيح. تحتفظ الأيقونة بحجمها المرئي بينما تصل المنطقة القابلة للنقر إلى 44 في 44."},{"text":"غلّف الأيقونة بـ Pressable ثانٍ غير مرئي.","why":"هدفا لمس متداخلان يجعلان من غير الواضح أيهما يستقبل الضغطة، ويضيفان view إضافيًا بلا فائدة."},{"text":"انتقل إلى <code dir=\"ltr\">onLongPress</code> ليكون أمام المستخدمين وقت أطول للتصويب.","why":"الضغط المطوّل يغيّر الإيماءة لا حجم الهدف. سيظل المستخدمون يخطئون الهدف، وسينتظرون أيضًا."}],"answer":0},{"q":"<p>ما المشكلة في حقل الإدخال هذا؟</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">TSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setName</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">''</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">TextInput</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onChange</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">setName</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> /></span></span></code></pre></div>\n","options":[{"text":"لا مشكلة؛ <code dir=\"ltr\">onChange</code> و<code dir=\"ltr\">onChangeText</code> اسمان لشيء واحد.","why":"ليسا كذلك. <code dir=\"ltr\">onChange</code> يستقبل كائن حدث أصليًا، فتصبح <code dir=\"ltr\">name</code> كائنًا."},{"text":"لا يمكن أن يكون TextInput مضبوطًا؛ بل يجب أن يدير قيمته بنفسه.","why":"الحقول المضبوطة هي النمط المعتاد في React Native، تمامًا كما على الويب."},{"text":"<code dir=\"ltr\">onChange</code> يمرّر حدثًا لا النص، فتصبح <code dir=\"ltr\">name</code> كائنًا؛ استخدم <code dir=\"ltr\">onChangeText={setName}</code>.","why":"صحيح. <code dir=\"ltr\">onChangeText</code> يعطيك النص الجديد مباشرة."}],"answer":2}],"exercise":{"kind":"web","mode":"js","starter":{"js":"// Turn what a hiker types into whole minutes, or null if it isn't valid.\n// Accept: \"135\", \"2:15\", \"2h 15m\", \"2h15m\", \"1h\", \"45m\" (any case, extra spaces OK).\nfunction parseDurationMin(text) {\n  return Number(text);\n}\n\nconsole.log(parseDurationMin('2h 15m'));\n"},"solution":{"js":"function parseDurationMin(text) {\n  const s = String(text).trim().toLowerCase();\n  let minutes = null;\n  let m;\n  if (/^\\d+$/.test(s)) {\n    minutes = Number(s);\n  } else if ((m = s.match(/^(\\d+):(\\d{2})$/))) {\n    if (Number(m[2]) < 60) minutes = Number(m[1]) * 60 + Number(m[2]);\n  } else if ((m = s.match(/^(?:(\\d+)\\s*h)?\\s*(?:(\\d+)\\s*m)?$/)) && (m[1] || m[2])) {\n    minutes = Number(m[1] ?? 0) * 60 + Number(m[2] ?? 0);\n  }\n  return minutes !== null && minutes > 0 ? minutes : null;\n}\n\nconsole.log(parseDurationMin('2h 15m'));\n"},"checks":[{"id":"plain-minutes","test":"return typeof parseDurationMin === 'function' && parseDurationMin(\"135\") === 135 && parseDurationMin(\" 90 \") === 90;"},{"id":"clock-format","test":"return parseDurationMin(\"2:15\") === 135 && parseDurationMin(\"0:45\") === 45 && parseDurationMin(\"2:75\") === null;"},{"id":"hours-minutes","test":"return parseDurationMin(\"2h 15m\") === 135 && parseDurationMin(\"2h15m\") === 135 && parseDurationMin(\"1H 5M\") === 65;"},{"id":"single-unit","test":"return parseDurationMin(\"1h\") === 60 && parseDurationMin(\"45m\") === 45;"},{"id":"rejects-bad","test":"return parseDurationMin(\"\") === null && parseDurationMin(\"abc\") === null && parseDurationMin(\"0\") === null && parseDurationMin(\"h\") === null;"}],"title":"حلّل مدّة الرحلة","prompt":"<p>في نموذج Trailhead حقل <strong>Duration</strong> مع <code dir=\"ltr\">keyboardType=&quot;default&quot;</code>، لأن المتنزّهين يكتبون المدّة بطرق مختلفة. اكتب الدالة <code dir=\"ltr\">parseDurationMin(text)</code> بحيث تُرجع المدّة <strong>بالدقائق الكاملة</strong>، أو <code dir=\"ltr\">null</code> حين لا يكون النص صالحًا.</p>\n<p>اقبل هذه الأشكال (مع تجاهل حالة الأحرف والمسافات المحيطة):</p>\n<ul>\n<li>دقائق فقط: <code dir=\"ltr\">&quot;135&quot;</code></li>\n<li>صيغة الساعة: <code dir=\"ltr\">&quot;2:15&quot;</code> (يجب أن تكون الدقائق أقل من 60، لذا <code dir=\"ltr\">&quot;2:75&quot;</code> تُرجع <code dir=\"ltr\">null</code>)</li>\n<li>ساعات ودقائق: <code dir=\"ltr\">&quot;2h 15m&quot;</code>، <code dir=\"ltr\">&quot;2h15m&quot;</code>، <code dir=\"ltr\">&quot;1h&quot;</code>، <code dir=\"ltr\">&quot;45m&quot;</code></li>\n</ul>\n<p>أرجع <code dir=\"ltr\">null</code> للنص الفارغ، ولأي شيء آخر، وللصفر.</p>\n","hints":["وحّد الصيغة أولًا: <code dir=\"ltr\">String(text).trim().toLowerCase()</code>.","تعامل مع كل شكل بتعبير نمطي (regular expression) خاص به وأرجع النتيجة مبكرًا، مثل <code dir=\"ltr\">/^(\\d+):(\\d{2})$/</code> لصيغة الساعة.","لصيغة h/m، يطابق <code dir=\"ltr\">/^(?:(\\d+)\\s*h)?\\s*(?:(\\d+)\\s*m)?$/</code> الجزأين اختياريًا؛ تأكّد أن مجموعة واحدة على الأقل طابقت فعلًا، وإلا تسلّل النص الفارغ."],"explanation":"<p>توحّد الدالة صيغة النص، ثم تجرّب نمطًا واحدًا لكل شكل مقبول، وتحوّل كلًّا منها إلى دقائق. كل مسار لا يطابق يترك <code dir=\"ltr\">minutes</code> بقيمة <code dir=\"ltr\">null</code>، وحارس أخير يرفض الصفر. إبقاء هذا خارج الـ component يعني أن النموذج يسأل سؤالًا واحدًا فقط (<code dir=\"ltr\">parseDurationMin(text) !== null</code>)، وأنك تستطيع كتابة unit tests لكل إدخال غريب دون تشغيل محاكٍ.</p>\n","checkLabels":{"plain-minutes":"الأرقام العادية دقائق (“135” → 135)","clock-format":"صيغة الساعة تعمل وترفض الدقائق ≥ 60","hours-minutes":"تُحلَّل “2h 15m” و&quot;2h15m&quot; و&quot;1H 5M&quot;","single-unit":"“1h” → 60 و&quot;45m&quot; → 45","rejects-bad":"الإدخال الفارغ وغير الرقمي والصفر تُرجع null"}},"words":624,"readMinutes":4}