UIDex

قائمة الأوامر

Menu (Dropdown Menu and Context Menu)

role="menu" + role="menuitem"

اسمه كمانمنيو، قائمة السياق، قائمة كليك يمين، قائمة الثلاث نقاط، قائمة الأوامر المنسدلة، دروب داون منيو، dropdown menu، context menu، right-click menu، action menu، overflow menu، kebab menu

قائمة الأوامر لوحة صغيرة تنفتح فوق الواجهة وتعرض أوامر جاهزة للتنفيذ، فما إن يضغط المستخدم سطراً منها حتى يقع الأمر وتُغلق اللوحة. ولها شكلان يتشاركان البنية نفسها: القائمة المنسدلة التي يفتحها زرّ ظاهر مثل زرّ الثلاث نقاط، وقائمة السياق التي يفتحها زرّ الفأرة الأيمن أو مفتاح ContextMenu على العنصر الذي يقف عليه المستخدم. ولا يخزّن أي بند فيها قيمة، ولهذا لا يقبل menuitem خاصّية aria-selected أصلاً. وهي ليست قائمة اختيار: عنصر select يحمل قيمة تبقى ظاهرة على الزرّ وتُرسَل مع النموذج، و listbox يعلّم اختياره بـ aria-selected ويبقيه بعد الإغلاق، والفيصل ما يحدث بعد الضغط: تنفيذ أمر أم تثبيت قيمة يقرأها شيء لاحقاً. وليست popover، فالـ popover صندوق عائم يقبل أي محتوى بلا دلالة، بينما role="menu" عقد صارم: أبناء من menuitem وما يشبهه، وتنقّل بالأسهم، ومحطّة Tab واحدة للقائمة كلّها. وليست قائمة روابط: شريط تنقّل الموقع عنصر <ul> فيه <a>، ووضع role="menu" على القائمة و role="menuitem" على الروابط يسلبها دور link فتسقط من قائمة الروابط في قارئ الشاشة، ويَعِد المستخدم بتنقّل بالأسهم لم يكتبه أحد. القاعدة العملية: role="menu" لأوامر التطبيق مثل «تحرير» و«ترتيب حسب»، والروابط تبقى روابط داخل <nav>.

لو قلت عليه…

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

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

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

تقرير-الربع.pdf

اضغط بالزرّ الأيمن في أي مكان هنا لفتح قائمة السياق.

آخر أمر: فتح · الشبكة: ظاهرة

RTLالقائمة الفرعية تفتح نحو نهاية السطر، وسهمها وحده هو ما ينعكس

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

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

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

ابنِ قائمة أوامر بشكليها: قائمة منسدلة من زرّ، وقائمة سياق من حدث contextmenu، بالبنية نفسها. المُشغِّل زرّ حقيقي يحمل aria-haspopup="menu" و aria-expanded يتبدّل و aria-controls يشير إلى معرّف القائمة، ويفتح بـ Enter و Space و ArrowDown على أوّل بند و ArrowUp على آخر بند. الحاوية role="menu" مسمّاة بـ aria-labelledby، وأبناؤها المباشرون menuitem أو menuitemcheckbox مع aria-checked أو menuitemradio داخل role="group" أو separator، ولا عنصر بلا دور بينها. اجعل القائمة محطّة Tab واحدة: بند واحد بـ tabindex="0" والباقي ‎-1‎، والأسهم تنقل التركيز فعلياً، و Home و End إلى الطرفين، والكتابة تقفز بالحرف الأوّل بعد تطبيع النصّ العربي بإزالة التشكيل وتوحيد الألف. Escape يغلق ويعيد التركيز إلى المُشغِّل، وكذلك الضغط في الخارج و Tab. القائمة الفرعية بند بـ aria-haspopup="menu" تليه حاوية role="menu" داخل غلاف role="none"، تفتح نحو النهاية المنطقية للسطر وتنقلب عند حافّة النافذة، ومعها مثلّث أمان يمنع إغلاقها أثناء الحركة القطرية، وبلا مستوى ثالث. في قائمة السياق استدعِ preventDefault وادعم مفتاح ContextMenu و Shift+F10، واحسب الموضع منطقياً: clientX يُقاس من الحافّة اليسرى الفيزيائية للنافذة دائماً، فحوّله عند rtl إلى inset-inline-start يساوي documentElement.clientWidth ناقص clientX. واستعمل الخصائص المنطقية في كل مكان: padding-inline-start لعمود العلامة، و margin-inline-start: auto للاختصار، و text-align: start للنصّ، ولا تكتب left ولا right ولا ml-auto. واعكس سهم القائمة الفرعية بصنف مخصّص وحده، واعزل نصّ الاختصار بـ dir="ltr" مع unicode-bidi: isolate.

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

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

جهة القائمة الفرعية

ينعكس

القائمة الفرعية تتابع اتجاه القراءة: تفتح عن يمين البند في الإنجليزية وعن يساره في العربية، أي عند نهايته المنطقية. والجهة تُحسب لحظة الفتح ولا تُكتب مرّة واحدة في CSS، لأنها تنقلب إلى الجهة المقابلة متى ضاق ما تبقّى من عرض النافذة. وانتبه إلى أن Floating UI منطقية في المحاذاة وحدها: لاحقتا ‎-start و ‎-end تقرآن direction من العنصر نفسه، أمّا الجهة الأساسية في placement فأسماؤها فيزيائية (top و right و bottom و left)، فإمّا أن تختار "left-start" بنفسك عند rtl، وإمّا أن تسلّم الأمر لمكوّن يقرأ الاتجاه مثل Radix: خاصّية dir تُوضع على DropdownMenu.Root أو على DirectionProvider يلفّ الشجرة، لا على DropdownMenu.Sub الذي لا يقبلها.

سهم القائمة الفرعية

ينعكس

السهم في طرف البند يشير إلى الجهة التي ستنفتح فيها القائمة الفرعية، فهو اتجاهيّ بحت ولا بدّ أن ينقلب مع الصفحة. ولو رسمته حرفاً نصّياً فلا تفترض أن محرّك bidi سيتكفّل به: خاصّية Bidi_Mirrored قيمتها Yes في › (U+203A) فينقلب من تلقاء نفسه، وقيمتها No في → (U+2192) و ▸ (U+25B8) فيبقيان كما رُسما مهما كان الاتجاه. اجعل الانعكاس صنفاً مخصّصاً مثل ‎.flip-x { transform: scaleX(-1) }‎ داخل [dir="rtl"] وضعه على هذا السهم وحده، فالقاعدة العامّة على svg ستقلب معه علامة الصح وأيقونتَي «نسخ» و«حذف».

عمود العلامة وعمود الاختصار

ينعكس

كل بند صفّ من ثلاث خانات: علامة الحالة عند البداية المنطقية، والنصّ في الوسط، وتلميح الاختصار عند النهاية المنطقية، فتتبادل الخانتان الطرفيتين مكانيهما مع اتجاه الصفحة. احجز خانة العلامة في كل البنود بـ padding-inline-start أو بعنصر فارغ بنفس العرض، وادفع الاختصار إلى الطرف بـ margin-inline-start: auto. والفخّ الجاهز هنا مكوّن DropdownMenuShortcut في shadcn/ui: صنفه ml-auto، وهو يعمل في الإنجليزية ويترك الاختصار ملتصقاً بالجهة الخطأ في العربية حتى تستبدله بـ ms-auto.

نصّ الاختصار نفسه

لا ينعكس

موضع الاختصار ينتقل مع الصفحة، أمّا ترتيب حروفه فلا يمسّه شيء: Ctrl+K تُقرأ كما هي في الاتجاهين. والخطر في الرموز التي تتصدّره، فـ ⌘ (U+2318) و ⇧ (U+21E7) صنفهما في bidi هو ON أي محرف محايد، والمحايد الواقف على حافّة النصّ يأخذ اتجاه الفقرة المحيطة به فيقفز إلى الطرف الآخر من الحرف وتقرأ العين ‎K⌘‎ بدل ‎⌘K‎. اعزل النصّ كتلة واحدة بـ dir="ltr" مع unicode-bidi: isolate على العنصر أو بلفّه في <bdi>، ولا تعالج الأمر بمسافة زائدة ولا بمحرف تحكّم تكتبه بيدك داخل النصّ.

مفتاحا ArrowRight و ArrowLeft

ينعكس

المفتاحان يتبادلان وظيفتيهما مع الاتجاه: في العربية يفتح ArrowLeft القائمة الفرعية ويغلقها ArrowRight، وفي الإنجليزية العكس. اقرأ الاتجاه مرّة واحدة من getComputedStyle(el).direction واشتقّ منه openKey و closeKey، بدل نثر شرط rtl في كل فرع من معالج المفاتيح. وما عدا هذين المفتاحين لا يتبدّل شيء: ArrowDown و ArrowUp و Home و End تعمل على ترتيب البنود في الـ DOM، و Escape يغلق ويعيد التركيز، والقفز بالحرف الأوّل لا علاقة له بالاتجاه إطلاقاً.

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

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

ARIArole="menu" + role="menuitem"العقد نفسه: أبناء القائمة بنود أو فواصل أو مجموعات، ولا شيء غيرها.
ARIAaria-haspopup="menu" + aria-expandedعلى المُشغِّل لا على القائمة؛ والقيمة true تعني menu ضمناً، والتصريح أوضح.
HTML<menu>عنصر HTML بهذا الاسم مجرّد <ul> بتسمية أخرى، ودوره الضمني list لا menu.
Radix UI<DropdownMenu.Root> / <ContextMenu.Root>بنية واحدة بمُشغِّلين، وخاصّية dir تقلب جهة القائمة الفرعية ومعنى السهمين الأفقيين.
shadcn/ui<DropdownMenuCheckboxItem> / <DropdownMenuShortcut>الأوّل يعطيك aria-checked جاهزاً، والثاني span بـ ml-auto يجب تحويله إلى ms-auto.
Material UI<Menu> + <MenuItem>يرسم ul بـ role="menu" وينقل التركيز إلى البند فعلياً بدل aria-activedescendant.
CSSunicode-bidi: isolateيعزل نصّ الاختصار اللاتيني فلا يقفز رمز ⌘ إلى الطرف الآخر من الحرف.

شوف كمان

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