UIDex

حقل الإكمال التلقائي

Combobox / Autocomplete

role="combobox"

اسمه كمانكومبوبوكس، أوتوكومبليت، الإكمال التلقائي، حقل بحث بقائمة، قائمة منسدلة قابلة للكتابة، حقل اقتراحات، autocomplete، typeahead، autosuggest، searchable select، filterable dropdown، lookup field

حقل الإكمال التلقائي حقل نصّ قابل للتحرير مربوط بلائحة خيارات تضيق مع كل حرف يكتبه المستخدم، وينتهي دائماً باختيار عنصر واحد من مجموعة تعرفها الواجهة سلفاً. وثلاثة عناصر تشبهه على الشاشة. الأوّل القائمة المنسدلة العادية (select): ليس فيها نصّ قابل للتحرير، والكتابة داخلها تنقل التحديد إلى أقرب خيار مطابق ولا تُصفّي اللائحة. والدور لا يفصل بينهما، لأن HTML يمنح <select> ذا السطر الواحد الدور نفسه role="combobox" الذي تعطيه ARIA 1.2 للنسخة المصنوعة يدوياً؛ والفارق وجود <input> يقبل التحرير وقيمة aria-autocomplete التي تصف نوع الإكمال. والثاني حقل البحث: يأخذ نصّاً حرّاً ويرسله استعلاماً، وما يخرج منه جملة كتبها المستخدم لا معرّف صفّ في قاعدة بيانات، ولهذا يصحّ فيه ما لا يطابق شيئاً. والثالث القائمة (menu): بنودها role="menuitem" تُطلق أوامر ولا تحمل قيمة، ولوحة الأوامر تُحسب معها رغم أنها تُبنى غالباً بدوري combobox و listbox، لأن Enter فيها يُنفّذ إجراءً بدل أن يكتب قيمة. ويحسم الأمر ما يبقى في الحقل بعد Enter: معرّف خيار يعني حقل إكمال، ونصّ كتبه المستخدم يعني حقل بحث، وحقل فارغ مع أمر جرى تنفيذه يعني قائمة.

لو قلت عليه…

«الخانة اللي بتكتب فيها فتنزل لك اقتراحات تحتها»«سيلكت بس بيقبل إني أكتب جوّاه»«حقل الدولة اللي بكتب فيه حرفين ويلاقيها»«اللستة اللي بتقلّ وانت بتكتب»«الحاجة اللي بتكمّل لي الكلمة زي بتاعة جوجل»

العيّنة الحيّة

تفاعل مع الديمو. كل الأجزاء حقيقية ومرقّمة.

مطار الوصول٣ نتائج
الدوحةDOH
دبيDXB
جدةJED
RTLاللائحة تلتصق ببداية الحقل، والرمز اللاتيني معزول بـ bdi

تشريح العنصر: كل جزء واسمه

مرّر على أي سطر ليتحدّد مكانه في الديمو فوق.

برومبت البناء

ابنِ حقل إكمال تلقائي (combobox) يختار عنصراً واحداً من مجموعة معروفة، لا حقل بحث حرّ. اجعل العنصر <input type="text"> يبقى قابلاً للتحرير دائماً ويحمل role="combobox" و aria-autocomplete="list" و aria-expanded يتبدّل فعلياً و aria-controls يشير إلى معرّف اللائحة و aria-labelledby يربطه بعنوان الحقل، ولا تضع عليه readonly أبداً. اللائحة عنصر role="listbox" وأبناؤها role="option" لكلٍّ منها id مشتقّ من مفتاح البيانات و aria-selected صريح على القيمة المثبّتة وحدها. أبقِ التركيز في الحقل وضع aria-activedescendant عليه هو، ومرّر الصفّ النشط داخل الرؤية بـ scrollIntoView({ block: "nearest" }). ادعم ArrowDown و ArrowUp و Enter و Escape (يُغلق ويُبقي النصّ، وضغطة ثانية تمسحه)، واترك ArrowLeft و ArrowRight و Home و End لمؤشّر النصّ. أخّر الطلب ٢٥٠ مللي ثانية، وألغِ السابق بـ AbortController، وتجاهل أي ردّ لا يخصّ آخر استعلام. ضع منطقة aria-live="polite" فارغة في الصفحة من البداية خارج اللائحة تعلن عدد النتائج، واجعل رسالة «لا نتائج» نصّاً عادياً لا role="option"، ولا تضع داخل عنصر role="listbox" غير أبناء role="option". وحّد النصّ قبل المطابقة: انزع التشكيل والتطويل، وردّ صور الهمزة إلى ا، وحوّل الأرقام العربية إلى ASCII، واحسب مواضع التمييز على النصّ المعروض. استخدم الخصائص المنطقية في كل شيء: inset-inline-start لمحاذاة اللائحة، و inset-inline-end لمؤشّر التحميل وزرّ المسح، و padding-inline للحشو، و text-align: start للنصّ، ولا تكتب left ولا right. ولو أرسلت اللائحة بـ portal فضع dir على عنصر الـ portal، ولا تقلب مؤشّر التحميل في RTL، واقصر تمييز المطابقة على لون الخلفية دون تغيير وزن الخطّ.

سلوكه في الاتجاه من اليمين لليسار

الجزء ده حصري عندنا.

محاذاة اللائحة والطبقة العليا

ينعكس

اللائحة تلتصق بالحافّة المنطقية الأولى للحقل: يمينه في العربية ويساره في الإنجليزية، فاكتبها inset-inline-start: 0 داخل حاوية position: relative. والفخّ في طريقة رفعها فوق بقيّة الصفحة: createPortal إلى document.body ينقل العقدة خارج شجرتك فتنقطع وراثة dir="rtl"، فيرتدّ النصّ إلى محاذاة اليسار، وتقرأ Floating UI قيمة direction بـ getComputedStyle من العنصر العائم نفسه فتحسبه ltr وتضع المحاذاة المنطقية start في الجهة الخطأ. ضع سمة dir على عنصر الـ portal نفسه، أو على <html> حتى لا يفلت منها شيء. والمخرج النظيف سمة popover مع CSS anchor positioning: الأولى ترفع الصندوق إلى الطبقة العليا وقت الرسم وهو باقٍ في مكانه من الشجرة فتسلم الوراثة، والثانية تحاذيه مع حقل يقع في أي موضع من المستند، و position-area تقبل الكلمات المنطقية مثل block-end span-inline-end فتنعكس وحدها.

مؤشّر التحميل

لا ينعكس

موضع مؤشّر التحميل ينعكس، ورسمته لا تنعكس. ثبّته بـ inset-inline-end حتى ينتقل إلى يسار الحقل في العربية، ولا تضع scaleX(-1) عليه ولا على أي عنصر يحويه: قلب المحور يعكس اتجاه الدوران فيلفّ عكس عقارب الساعة ويبدو معطوباً. والقاعدة الشاملة [dir="rtl"] svg { transform: scaleX(-1) } هي السبب المعتاد، فاقصر القلب على صنف تضعه على الأسهم وحدها. والمؤشّر المرسوم بـ conic-gradient لا تمسّه قيمة direction إطلاقاً، لأن زوايا التدرّج المخروطي تدور مع عقارب الساعة من الأعلى دائماً وليس للتدرّجات صيغة منطقية أصلاً، لكنه يبقى عرضة للقلب نفسه لو طاله scaleX.

التمييز داخل الكلمة

لا ينعكس

تمييز مقطع من كلمة عربية يقطعها إلى صناديق سطرية، وهذا وحده لا يكسر الوصل، لأن المتصفّح يصل الحروف عبر حدّ العنصر ما دامت خصائص الخطّ متطابقة ولا يفصل بين الطرفين هامش ولا إطار ولا حشو على المحور السطري. لكن إعطاء المقطع المميّز font-weight: 700 يجلب وجهاً آخر من الخطّ فينقطع الوصل وتظهر «الدوحة» مقطوعة عند موضع المطابقة، وكذلك يفعل letter-spacing و padding داخل السطر. اقصر التمييز على background-color و color. ولا تلفّ المقطع بـ <bdi> ولا تضع عليه unicode-bidi: isolate، فالعزل حدّ اتجاه يقطع الوصل ويجعل المقطع مقطعاً ثنائي الاتجاه مستقلاً قد يُعاد ترتيبه بعيداً عن بقيّة الكلمة.

استعلام عربي فوق تسميات لاتينية

لا ينعكس

المطابقة تفشل في العربية قبل أن يصل الدور إلى الاتجاه: «احمد» لا تطابق «أحمد» لأن includes تقارن نقاط الترميز. و Intl.Collator يسوّي بين أ و إ و ا على sensitivity: "base"، لكنه يبقي آ و ٱ مختلفتين، وهو على أي حال يقارن السلاسل كاملة ولا يبحث عن مقطع داخلها. وحّد الطرفين قبل المقارنة: انزع التشكيل بحذف المدى U+064B إلى U+0652، واحذف التطويل U+0640، وردّ أ إ آ ٱ إلى ا و ى إلى ي، وحوّل الأرقام العربية والفارسية إلى ASCII، مع toLowerCase للطرف اللاتيني. ولفّ الرمز اللاتيني في الصفّ المختلط بـ <bdi>، وإلا جرّته الأرقام أو الفاصلة المجاورة إلى الطرف الآخر من السطر.

مؤشّر النصّ ومفاتيحه

لا ينعكس

الحقل يملك مؤشّر النصّ، فتبقى ArrowLeft و ArrowRight للتنقّل بين الحروف ولا تُسنَدان إلى اللائحة إطلاقاً، ولا يحتاج الأمر إلى شرط على الاتجاه في أي مكان. و Home و End هنا تذهبان إلى أوّل النصّ وآخره لا إلى أوّل خيار وآخره، وهذا عكس ما يفعله listbox المجرّد. و selectionStart و selectionEnd إزاحتان منطقيتان داخل السلسلة لا تتغيّران مع اتجاه الصفحة، لكن موضع المؤشّر البصري في نصّ مختلط لا يتزايد معهما، فلا تحسب إحداثيّه بقياس عرض المقطع الذي قبله. ولو كان الحقل نفسه يقبل العربية والرموز اللاتينية معاً، فسمة dir="auto" عليه هي الحلّ: يقرأ المتصفّح أوّل حرف قويّ في القيمة ويضبط اتجاه الحقل عليه، متجاوزاً الأرقام وعلامات الترقيم في طريقه إليه.

أسماؤه في الكود

كل سطر هو كلمة مكتبة واحدة عن نفس الشيء. خد السطر اللي بيكلّم مشروعك.

ARIArole="combobox" + aria-autocomplete="list"list تعني أن الكتابة تُصفّي اللائحة فقط، و both تضيف إكمالاً داخل الحقل معها، و inline تُكمل بلا لائحة أصلاً.
ARIAaria-activedescendantمكانه الحقل لا اللائحة، لأن التركيز يبقى في الحقل حتى وأنت تتنقّل بالأسهم.
HTML<input list="…"> + <datalist>أرخص تنفيذ صحيح، لكن المتصفّح يملك شكل اللائحة وقاعدة المطابقة ولا يترك لك خطّافاً لتنسيقهما.
shadcn/ui<Command> inside <Popover>لا يوجد Combobox جاهز في المكتبة؛ يُركَّب من cmdk داخل Popover، والتصفية تُستبدل عبر shouldFilter و filter.
Material UI<Autocomplete filterOptions freeSolo>freeSolo يسمح بقيمة لم ترد في اللائحة فيحوّله إلى حقل بحث، و filterOptions هو الموضع الذي تدسّ فيه توحيد الهمزات.
Ant Design<AutoComplete> vs <Select showSearch>الأوّل يقبل ما كتبه المستخدم كقيمة نهائية، والثاني لا يُخرج إلا مفتاح خيار موجود في اللائحة.
CSSunicode-bidi: isolateيعزل الرمز اللاتيني داخل تسمية عربية، ولا يوضع أبداً على مقطع مميّز داخل كلمة.

شوف كمان

آخر تحديث · 2026-08-19