إنشاء مكون مربع حوار

نظرة عامة أساسية حول كيفية إنشاء مربّعات نوافذ مشروطة صغيرة وكبيرة تتكيّف مع الألوان وتتوافق مع جميع الأجهزة وتتيح إمكانية الوصول إليها باستخدام العنصر <dialog>

في هذه المشاركة، أودّ مشاركة أفكاري حول كيفية إنشاء نماذج مصغّرة ونموذجية سريعة الاستجابة ومتوافقة مع الألوان باستخدام العنصر <dialog>. جرِّب الإصدار التجريبي واطّلِع على الرمز المصدر.

عرض لمربّعات الحوار الضخم والمصغَّر في المظهرَين الفاتح والداكن

إليك نسخة من هذه المشاركة على YouTube إذا كنت تفضّل ذلك:

نظرة عامة

عنصر <dialog> يُعدّ مثاليًا للمعلومات أو الإجراءات السياقية داخل الصفحة. ضَع في اعتبارك الحالات التي يمكن أن تستفيد فيها تجربة المستخدم من إجراء على الصفحة نفسها بدلاً من إجراء متعدد الصفحات: ربما يكون النموذج صغيرًا أو أن الإجراء الوحيد المطلوب من المستخدم هو التأكيد أو الإلغاء.

أصبح عنصر <dialog> ثابتًا مؤخرًا في جميع المتصفّحات:

توافق المتصفّح

  • Chrome: 37.
  • ‫Edge: 79
  • Firefox: 98
  • Safari: الإصدار 15.4.

المصدر

تبيّن لي أنّ العنصر لا يتضمّن بعض العناصر، لذلك أضفت في هذا GUI التحدّي عناصر تجربة المطوّر التي أتوقّعها: أحداث إضافية وإغلاق خفيف ورسوم متحركة مخصّصة ونوعين صغيرين وكبيرين.

Markup

إنّ العناصر الأساسية لعنصر <dialog> بسيطة. سيتم تلقائيًا إخفاء العنصر الذي يتضمّن أنماطًا مدمجة لوضعها فوق المحتوى.

<dialog>
  …
</dialog>

يمكننا تحسين هذا المتوقع.

عادةً، يتشارك عنصر الحوار كثيرًا مع شكل، وغالبًا ما تكون الأسماء قابلة للتبادل. لقد استخدمتُ عنصر مربّع الحوار لأجل كلٍّ من مربّعات الحوار المنبثقة الصغيرة (المربّعات الصغيرة) ومربّعات الحوار التي تظهر على مستوى الصفحة بالكامل (المربّعات الكبيرة). لقد سمّيت المحادثتَين "كبيرة" و"صغيرة"، وتم تعديلهما قليلاً لكي تتناسبا مع حالات الاستخدام المختلفة. لقد أضفت سمة modal-mode للسماح لك بتحديد النوع:

<dialog id="MegaDialog" modal-mode="mega"></dialog>
<dialog id="MiniDialog" modal-mode="mini"></dialog>

لقطة شاشة لكل من مربّع الحوار الصغير والكبير في كل من المظهرَين الفاتح والداكن

ليس دائمًا، ولكن بشكل عام سيتم استخدام عناصر الحوار لجمع بعض معلومات التفاعل. إنّ النماذج داخل عناصر مربّعات الحوار مصمّمة للاستخدام معًا. من الجيد أن يلفّ عنصر نموذج محتوى مربّع الحوار لكي تتمكّن لغة JavaScript من الوصول إلى البيانات التي أدخلها المستخدم. بالإضافة إلى ذلك، يمكن للأزرار داخل نموذج يستخدم method="dialog" إغلاق مربّع حوار بدون JavaScript ونقل البيانات.

<dialog id="MegaDialog" modal-mode="mega">
  <form method="dialog">
    …
    <button value="cancel">Cancel</button>
    <button value="confirm">Confirm</button>
  </form>
</dialog>

مربّع حوار Mega

يتضمّن مربّع الحوار الكبير ثلاثة عناصر داخل النموذج: <header>، <article>، و <footer>. وتُستخدَم هذه الحاويات على أنّها حاويات دلالية، بالإضافة إلى استهدافات الأنماط لعرض الحوار. يعرض العنوان عنوان النافذة المنبثقة ويقدّم زر إغلاق. هذه المقالة مخصّصة لمعلومات الإدخال في النماذج. يحتوي التذييل على <menu> من buttons الإجراءات.

<dialog id="MegaDialog" modal-mode="mega">
  <form method="dialog">
    <header>
      <h3>Dialog title</h3>
      <button onclick="this.closest('dialog').close('close')"></button>
    </header>
    <article>...</article>
    <footer>
      <menu>
        <button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
        <button type="submit" value="confirm">Confirm</button>
      </menu>
    </footer>
  </form>
</dialog>

يحتوي زر القائمة الأول على autofocus ومعالج أحداث onclick مضمّن. ستتلقّى السمة autofocus التركيز عند فتح مربّع الحوار، وأعتقد أنّ أفضل الممارسات هي وضع هذا التركيز على زر الإلغاء، وليس زر التأكيد. يضمن ذلك أنّ التأكيد هو مقصود وليس عرضيًا.

مربّع الحوار المصغّر

يتشابه مربّع الحوار المصغّر كثيرًا مع مربّع الحوار الكبير، إلا أنّه لا يتضمّن عنصر <header>. يتيح ذلك تصغير الرمز البرمجي وجعله أكثر ملاءمةً للعرض في النص.

<dialog id="MiniDialog" modal-mode="mini">
  <form method="dialog">
    <article>
      <p>Are you sure you want to remove this user?</p>
    </article>
    <footer>
      <menu>
        <button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
        <button type="submit" value="confirm">Confirm</button>
      </menu>
    </footer>
  </form>
</dialog>

يوفّر عنصر مربّع الحوار أساسًا قويًا لعنصر إطار العرض الكامل الذي يمكنه جمع البيانات وتفاعل المستخدمين. يمكن أن تؤدي هذه الأساسيات إلى بعض التفاعلات المشوّقة والفعّالة في موقعك الإلكتروني أو تطبيقك.

تسهيل الاستخدام

يتضمّن عنصر مربّع الحوار ميزات مدمجة جيدة جدًا لتسهيل الاستخدام. بدلاً من إضافة هذه الميزات كما أفعل عادةً، تتوفّر العديد منها.

استعادة التركيز

كما فعلنا يدويًا في إنشاء عنصر قائمة جانبية ، من المهم أن يؤدي فتح عنصر وإغلاقه بشكل صحيح إلى التركيز على زرَّي الفتح والإغلاق المعنيَّين. عند فتح شريط التنقّل الجانبي هذا، يتم التركيز على زر الإغلاق. عند الضغط على زر الإغلاق، تتم إعادة التركيز إلى الزر الذي فتح النافذة المنبثقة.

مع عنصر مربّع الحوار، يكون هذا السلوك التلقائي مضمّنًا:

إذا أردت إضافة تأثيرات متحركة إلى مربّع الحوار عند ظهوره أو اختفائه، لن تتمكّن من استخدام هذه الوظيفة. في قسم JavaScript سأستعيد هذه الوظيفة.

التركيز المستمر

يدير عنصر مربّع الحوار inert نيابةً عنك في المستند. قبل inert، كان يتم استخدام JavaScript لمراقبة تركيز العميل عند مغادرته عنصرًا، وعند هذه النقطة يتم اعتراضه وإعادته.

دعم المتصفح

  • Chrome: 102.
  • Edge: 102.
  • ‫Firefox: 112
  • Safari: الإصدار 15.5

المصدر

بعد inert، يمكن "تجميد" أي أجزاء من المستند لدرجة أنّها لم تعُد أهدافًا تركّز على التركيز أو تفاعلية باستخدام الماوس. بدلاً من تثبيت التركيز، يتم توجيهه إلى الجزء التفاعلي الوحيد من المستند.

فتح عنصر والتركيز عليه تلقائيًا

بشكل تلقائي، سيعين عنصر مربع الحوار التركيز على أول عنصر يمكن التركيز عليه في ترميز مربع الحوار. إذا لم يكن هذا هو العنصر الأفضل للمستخدم ليستخدمه تلقائيًا، استخدِم سمة autofocus. كما هو موضّح سابقًا، أرى أنّه من أفضل الممارسات وضع هذا الرمز على زر الإلغاء وليس زر التأكيد. وهذا يضمن أنّه تم تقديم التأكيد عن قصد وليس عن طريق الخطأ.

يتم الإغلاق باستخدام مفتاح Escape.

من المهم تسهيل إغلاق هذا العنصر الذي قد يتسبب في انقطاع المحتوى. لحسن الحظ، سيتعامل عنصر مربع الحوار مع مفتاح Escape نيابةً عنك، ما يخلصك من عبء التنظيم.

الأنماط

هناك مسار سهل لتصميم عنصر مربع الحوار والمسار الثابت. يمكن اتّباع المسار السهل من خلال عدم تغيير سمة العرض لمربّع الحوار والعمل مع قيوده. أختار الطريق الصعب لتقديم صور متحركة مخصّصة ل فتح مربّع الحوار وإغلاقه، والاستيلاء على الموقع display وغير ذلك.

تنسيق العناصر باستخدام "عناصر مفتوحة"

لتسريع تطور الألوان المتكيفة واتساق التصميم بشكل عام، أنشأت بدون خجل مكتبة متغيرات CSS Open Props. بالإضافة إلى المتغيّرات المجانية المقدّمة، أستورد أيضًا ملف normalize وبعض الزّرّات، وكلاهما يوفّره Open Props كعمليات استيراد اختيارية. تساعدني عمليات الاستيراد هذه في التركيز على تخصيص المربّع الحواري والعرض التجريبي بدون الحاجة إلى الكثير من الأنماط لدعمه وجعله يبدو جيدًا.

تصميم العنصر <dialog>

امتلاك الموقع المعروض

يؤدي السلوك التلقائي للعرض والإخفاء لعنصر مربّع الحوار إلى تبديل ملف تعريف الشاشة من block إلى none. وهذا يعني أنّه لا يمكن أن يظهر بشكل متحرك من الداخل إلى الخارج، بل من الداخل فقط. أريد إضافة تأثيرات متحركة للعنصر عند ظهوره وعند اختفائه، والخطوة الأولى هي ضبط سمة display الخاصة بي:

dialog {
  display: grid;
}

من خلال تغيير قيمة سمة العرض، وبالتالي امتلاكها، كما هو موضّح في المقتطف السابق من CSS، يجب إدارة قدر كبير من الأنماط من أجل تسهيل تجربة المستخدم المناسبة. أولاً، تكون الحالة التلقائية لمربّع الحوار هي مغلقة. يمكنك تمثيل هذه الحالة بشكل مرئي ومنع مربّع الحوار من تلقّي تفاعلات باستخدام الأنماط التالية:

dialog:not([open]) {
  pointer-events: none;
  opacity: 0;
}

أصبح مربّع الحوار غير مرئي ولا يمكن التفاعل معه عندما لا يكون مفتوحًا. سأضيف لاحقًا بعض JavaScript لإدارة سمة inert في مربّع الحوار، ما يضمن عدم تمكّن مستخدمي لوحة المفاتيح وبرامج قراءة الشاشة أيضًا من الوصول إلى مربّع الحوار المخفي.

منح مربّع الحوار مظهر ألوان تكيُّفيًا

مربّع حوار كبير يعرض المظهرَين الفاتح والداكن، ويوضّح ألوان السطح

مع أنّ تطبيق color-scheme يعمل على تفعيل مظهر المستند مع مظهر الألوان التكيُّفية التي يوفّرها المتصفّح، مع الإعدادات المفضّلة للنظام الفاتح والداكن، أردت تخصيص عنصر مربّع الحوار بشكل أكبر. توفّر Open Props بعض ألوان سطح التي تتكيّف تلقائيًا مع إعدادات النظام المفضّلة للوضعَين الفاتح والداكن، تمامًا مثل استخدام color-scheme. هذه الأدوات رائعة لإنشاء طبقات في التصميم، وأحب استخدام الألوان للمساعدة في تعزيز مظهر مساحات الطبقات بصريًا. لون الخلفية هو var(--surface-1). لتحديد أعلى هذه الطبقة، استخدِم var(--surface-2):

dialog {
  
  background: var(--surface-2);
  color: var(--text-1);
}

@media (prefers-color-scheme: dark) {
  dialog {
    border-block-start: var(--border-size-1) solid var(--surface-3);
  }
}

ستتم إضافة المزيد من الألوان التكيُّفية لاحقًا للعناصر الفرعية، مثل العنوان والتذييل. أعتبرها إضافية لعنصر مربّع الحوار، ولكنها مهمة جدًا في إنشاء تصميم مربّع حوار مقنع ومصمّم جيدًا.

ضبط حجم مربّع الحوار وفقًا لحجم الشاشة

يعتمد مربّع الحوار تلقائيًا على تحديد حجمه حسب المحتوى، وهو أمر جيد بشكل عام. هدفي هنا هو حصر max-inline-size بحجم قابل للقراءة (--size-content-3 = 60ch) أو بنسبة% 90 من عرض إطار العرض. يضمن ذلك عدم امتداد مربّع الحوار من حافة إلى حافة على جهاز جوّال، ولن يكون بعرض كبير على شاشة كمبيوتر مكتبي لدرجة يصعب معها قراءته. بعد ذلك، أضيف max-block-size حتى لا يتجاوز مربّع الحوار ارتفاع الصفحة. ويعني ذلك أيضًا أنّنا سنحتاج إلى تحديد مكان المنطقة التي يمكن التمرير فيها في مربّع الحوار، في حال كان عنصر مربّع الحوار طويلًا.

dialog {
  
  max-inline-size: min(90vw, var(--size-content-3));
  max-block-size: min(80vh, 100%);
  max-block-size: min(80dvb, 100%);
  overflow: hidden;
}

هل لاحظت أنّ لديّ max-block-size مرتين؟ يستخدم الأول 80vh، وهي وحدة ملف شخصي لعرض الصفحة. ما أريد فعلاً هو إبقاء المحادثة ضمن مسار نسبي، للمستخدمين الدوليين، لذلك أستخدم وحدة dvb المنطقية والأحدث والمتوافقة فقط جزئيًا في البيان الثاني عندما تصبح أكثر ثباتًا.

موضع مربّع الحوار الكبير

للمساعدة في تحديد موضع عنصر مربّع الحوار، من المفيد تقسيمه إلى جزئين: الخلفية بملء الشاشة وحاوية مربّع الحوار. يجب أن تغطي الخلفية كل العناصر، مع توفير تأثير تظليل للمساعدة في إبراز مربّع الحوار الذي يظهر في المقدّمة وعدم السماح بالوصول إلى المحتوى الذي يظهر في الخلفية. يمكن لحاوية مربّع الحوار أن ترتب نفسها في وسط هذه الخلفية وأن تتخذ الشكل الذي تتطلّبه محتوياتها.

تؤدي الأنماط التالية إلى تثبيت عنصر مربّع الحوار في النافذة، وتوسيعه إلى كل زاوية، واستخدام margin: auto لتوسيط المحتوى:

dialog {
  
  margin: auto;
  padding: 0;
  position: fixed;
  inset: 0;
  z-index: var(--layer-important);
}
أنماط مربّعات الحوار الضخمة على الأجهزة الجوّالة

في مساحات العرض الصغيرة، أُضفي طابعًا مختلفًا قليلاً على هذا المكوّن الإضافي الكبير للصفحة بالكامل. لقد ضبطت الهامش السفلي على 0، ما يؤدي إلى عرض محتوى مربّع الحوار في أسفل إطار العرض. من خلال تعديلين في النمط، يمكنني تحويل مربع الحوار إلى ورقة إجراءات، بالقرب من أصابع المستخدم:

@media (max-width: 768px) {
  dialog[modal-mode="mega"] {
    margin-block-end: 0;
    border-end-end-radius: 0;
    border-end-start-radius: 0;
  }
}

لقطة شاشة لأدوات المطوّرين التي تتراكب على مسافة الهامش 
  في مربّع الحوار الكبير على كلٍّ من الكمبيوتر المكتبي والأجهزة الجوّالة أثناء فتحه

موضع مربّع الحوار المصغّر

عند استخدام إطار عرض أكبر كما هو الحال على جهاز كمبيوتر مكتبي، اخترت وضع مربعات الحوار الصغيرة فوق العنصر الذي استدعائها. لإجراء ذلك، أحتاج إلى JavaScript. يمكنك الاطّلاع على الأسلوب الذي أستخدمه هنا، لكنّني أعتقد أنّه خارج نطاق هذه المقالة. بدون JavaScript، يظهر مربّع الحوار الصغير في وسط الشاشة، تمامًا مثل مربّع الحوار الكبير.

إضفاء لمسة مميزة

أخيرًا، أضِف بعض اللمسات إلى مربّع الحوار لكي يبدو كسطح ناعم فوق الصفحة. تتم معالجة التجانس من خلال تقريب زوايا مربّع الحوار. يتم تحقيق العمق باستخدام أحد مكوّنات الظل المصمّمة بعناية في Open Props:

dialog {
  
  border-radius: var(--radius-3);
  box-shadow: var(--shadow-6);
}

تخصيص العنصر النائب للخلفية

اخترت العمل بشكل بسيط جدًا مع الخلفية، ولم أُضِف سوى تأثير التمويه باستخدام backdrop-filter في مربّع الحوار الكبير:

توافق المتصفّح

  • Chrome: 76
  • الحافة: 79.
  • Firefox: 103
  • Safari: 18

المصدر

dialog[modal-mode="mega"]::backdrop {
  backdrop-filter: blur(25px);
}

اخترت أيضًا إضافة انتقال إلى backdrop-filter، على أمل أن تسمح المتصفّحات بنقل عنصر الخلفية في المستقبل:

dialog::backdrop {
  transition: backdrop-filter .5s ease;
}

لقطة شاشة لمربّع الحوار الكبير الذي يظهر فوق خلفية مموهة تتضمّن صور أفاتار ملونة

ميزات إضافية للتصميم

أطلق على هذا القسم اسم "الإضافات" لأنه يتعلق بالعرض التوضيحي لعنصر مربع الحوار أكثر من ارتباطه بعنصر مربع الحوار بشكل عام.

احتواء التمرير

عندما يتم عرض مربع الحوار، يظل المستخدم قادرًا على تمرير الصفحة خلفه، وهو ما لا أريده:

عادةً ما يكون overscroll-behavior هو الحل المعتاد، ولكن وفقًا للمواصفات، لن يكون له أي تأثير في مربّع الحوار لأنّه ليس منفذ تمرير، أي أنّه ليس أداة تمرير، وبالتالي ما مِن شيء يمنعه. يمكنني استخدام JavaScript لملاحظة الأحداث الجديدة من هذا الدليل، مثل "مغلقة" و"مفتوحة"، وتبديل overflow: hidden في المستند، أو يمكنني الانتظار إلى أن يصبح :has() ثابتًا في جميع المتصفّحات:

توافق المتصفّح

  • Chrome: 105.
  • ‫Edge: 105
  • ‫Firefox: 121
  • ‫Safari: 15.4

المصدر

html:has(dialog[open][modal-mode="mega"]) {
  overflow: hidden;
}

الآن عندما يكون مربّع حوار كبير مفتوحًا، يحتوي مستند html على overflow: hidden.

التنسيق <form>

بالإضافة إلى أنّه عنصر مهم جدًا لجمع معلومات التفاعل من المستخدم، أستخدمه هنا لعرض عناصر العنوان والتذييل والمقالة. باستخدام هذا التنسيق، أنوي توضيح العنصر الثانوي كمنطقة قابلة للتمرير. أحقّق ذلك باستخدام grid-template-rows. يتم منح عنصر المقالة 1fr، ويكون الحد الأقصى للارتفاع في النموذج نفسه هو نفسه الحد الأقصى لعنصر مربّع الحوار. إنّ ضبط هذا الارتفاع الثابت وحجم الصف الثابت هو ما يسمح بتقييد عنصر المقالة والانتقال للأسفل أو للأعلى عند تجاوزه الحدّ الأقصى:

dialog > form {
  display: grid;
  grid-template-rows: auto 1fr auto;
  align-items: start;
  max-block-size: 80vh;
  max-block-size: 80dvb;
}

لقطة شاشة لأدوات مطوري البرامج متراكبة على معلومات تنسيق الشبكة على الصفوف.

تصميم مربّع الحوار <header>

ودور هذا العنصر هو تقديم عنوان لمحتوى مربّع الحوار وتوفير زر إغلاق يسهل العثور عليه. ويتم أيضًا منحه لون سطح لإظهاره خلف محتوى مقالة مربع الحوار. تؤدي هذه المتطلبات إلى استخدام حاوية صندوق مرن وعناصر مُحاذاة عموديًا مع ترك مسافة بينها وبين الحواف، وبعض المسافات الفارغة بين العناصر لإتاحة بعض المساحة للعنوان وأزرار الإغلاق:

dialog > form > header {
  display: flex;
  gap: var(--size-3);
  justify-content: space-between;
  align-items: flex-start;
  background: var(--surface-2);
  padding-block: var(--size-3);
  padding-inline: var(--size-5);
}

@media (prefers-color-scheme: dark) {
  dialog > form > header {
    background: var(--surface-1);
  }
}

لقطة شاشة لأدوات مطوّري البرامج في Chrome التي تُظهر معلومات تنسيق Flexbox على عنوان مربّع الحوار

تصميم زر إغلاق الرأس

بما أنّ العرض التجريبي يستخدم أزرار Open Props (عناصر العرض المفتوحة)، تم تخصيص زر الإغلاق ليصبح زرًا دائريًا يتضمّن رمزًا في المنتصف على النحو التالي:

dialog > form > header > button {
  border-radius: var(--radius-round);
  padding: .75ch;
  aspect-ratio: 1;
  flex-shrink: 0;
  place-items: center;
  stroke: currentColor;
  stroke-width: 3px;
}

لقطة شاشة لأدوات مطوّري البرامج في Chrome التي تعرض معلومات الحجم والحشو لزر إغلاق العنوان

تصميم مربّع الحوار <article>

يلعب عنصر المقالة دورًا خاصًا في مربّع الحوار هذا: فهو مساحة مخصّصة للتمرير فيها في حال كان مربّع الحوار طويلاً أو مرتفعًا.

لتحقيق ذلك، وضع عنصر النموذج الرئيسي حدًا أقصى لنفسه، وهو ما يوفر قيودًا على عنصر المقالة هذا للوصول إليه إذا زاد عن الحد المسموح به. اضبط overflow-y: auto بحيث يتم عرض أشرطة التمرير عند الحاجة فقط، وتحتوي على التمرير داخلها باستخدام overscroll-behavior: contain، والباقي ستكون أنماط عروض تقديمية مخصّصة:

dialog > form > article {
  overflow-y: auto; 
  max-block-size: 100%; /* safari */
  overscroll-behavior-y: contain;
  display: grid;
  justify-items: flex-start;
  gap: var(--size-3);
  box-shadow: var(--shadow-2);
  z-index: var(--layer-1);
  padding-inline: var(--size-5);
  padding-block: var(--size-3);
}

@media (prefers-color-scheme: light) {
  dialog > form > article {
    background: var(--surface-1);
  }
}

ودور التذييل هو أن يحتوي على قوائم أزرار الإجراءات. يتم استخدام Flexbox لمحاولة محاذاة المحتوى مع نهاية محور التسمية في التذييل، ثم إضافة بعض المسافات لمحاولة منح الأزرار بعض المساحة.

dialog > form > footer {
  background: var(--surface-2);
  display: flex;
  flex-wrap: wrap;
  gap: var(--size-3);
  justify-content: space-between;
  align-items: flex-start;
  padding-inline: var(--size-5);
  padding-block: var(--size-3);
}

@media (prefers-color-scheme: dark) {
  dialog > form > footer {
    background: var(--surface-1);
  }
}

لقطة شاشة لأدوات Chrome Devtools التي تُظهر معلومات تنسيق Flexbox على عنصر التذييل

يُستخدم عنصر menu لتضمين أزرار الإجراءات الخاصة بمربّع الحوار. ويستخدم تنسيق flexbox المرن مع gap لتوفير مساحة بين الأزرار. عناصر القائمة تحتوي على مساحة متروكة مثل <ul>. وأزيل هذا النمط أيضًا لأنّني لا أحتاج إليه.

dialog > form > footer > menu {
  display: flex;
  flex-wrap: wrap;
  gap: var(--size-3);
  padding-inline-start: 0;
}

dialog > form > footer > menu:only-child {
  margin-inline-start: auto;
}

لقطة شاشة لـ Chrome Devtools أثناء تراكب معلومات مربع flexbox على عناصر قائمة التذييل.

Animation

غالبًا ما تكون عناصر مربّع الحوار متحركة لأنها تدخل إلى النافذة وتخرج منها. من خلال إضافة بعض الحركات الداعمة إلى مربّعات الحوار عند الدخول والخروج، يساعد ذلك المستخدمين في التعرّف على مسار التفاعل.

لا يمكن عادةً إضافة تأثيرات متحركة إلى عنصر مربّع الحوار إلا عند ظهوره، وليس عند اختفائه. ويرجع ذلك إلى أنّ المتصفح يبدِّل سمة display في العنصر. في وقت سابق، كان يتم عرض مجموعة الدليل إلى الشبكة، ولا يضبطها أبدًا على "لا شيء". هذا يفتح القدرة على الرسوم المتحركة داخل وخارج.

تتضمّن Open Props العديد من ملفات keyframe المتحرّكة للاستخدام، ما يسهّل التنسيق ويجعله سهل القراءة. فيما يلي أهداف الرسوم المتحركة والنهج متعدد الطبقات الذي اتخذته:

  1. "الحدّ من الحركة" هو الانتقال التلقائي، وهو عبارة عن تمويه بسيط يظهر ويختفي.
  2. إذا كانت الحركة مقبولة، تتم إضافة رسوم متحركة للانزلاق والتكبير/التصغير.
  3. تم تعديل تنسيق الشاشة المتجاوبة للأجهزة الجوّالة لمربّع الحوار الكبير بحيث ينزلق للخارج.

عملية انتقال تلقائية آمنة ومفيدة

على الرغم من أنّ "العناصر القابلة للاستخدام" المفتوحة تتضمّن لقطات رئيسية للاختفاء والتلاشي، أفضّل استخدام هذه العناصر كطريقة مفضّلة للانتقالات التلقائية مع استخدام لقطات رئيسية متحركة كأحد التحسينات المحتملة. سبق أن صمّمنا مستوى رؤية مربّع الحوار باستخدام الشفافية، وضبطنا 1 أو 0 استنادًا إلى السمة [open]. للانتقال بين 0% و100%، حدِّد للمتصفّح المدّة ونوع التأثير الذي تريده:

dialog {
  transition: opacity .5s var(--ease-3);
}

إضافة حركة إلى الانتقال

إذا كان المستخدم لا يمانع استخدام الصور المتحركة، يجب أن ينزلق كلا مربّعَي الحوار الكبير والصغير للأعلى عند ظهورهما، وأن يتم تكبيرهما عند اختفائهما. يمكنك تحقيق ذلك باستخدام prefers-reduced-motion طلب الوسائط وبعض عناصر Open Props:

@media (prefers-reduced-motion: no-preference) {
  dialog {
    animation: var(--animation-scale-down) forwards;
    animation-timing-function: var(--ease-squish-3);
  }

  dialog[open] {
    animation: var(--animation-slide-in-up) forwards;
  }
}

تكييف الصورة المتحركة عند الخروج مع الأجهزة الجوّالة

في وقت سابق من قسم النمط، تم تكييف نمط مربع الحوار الضخم للأجهزة المحمولة ليكون مثل ورقة إجراءات، كما لو أن قطعة صغيرة من الورق قد انزلقت لأعلى من الجزء السفلي من الشاشة ولا تزال متصلة بالجزء السفلي. لا تتناسب الرسم المتحرك للخروج مع هذا التصميم الجديد جيدًا، ويمكننا تكييف ذلك مع استعلامين عن الوسائط وبعض الدعائم المفتوحة:

@media (prefers-reduced-motion: no-preference) and @media (max-width: 768px) {
  dialog[modal-mode="mega"] {
    animation: var(--animation-slide-out-down) forwards;
    animation-timing-function: var(--ease-squish-2);
  }
}

JavaScript

هناك العديد من الأشياء التي يمكن إضافتها باستخدام JavaScript:

// dialog.js
export default async function (dialog) {
  // add light dismiss
  // add closing and closed events
  // add opening and opened events
  // add removed event
  // removing loading attribute
}

تأتي هذه الإضافات من الرغبة في إغلاق خفيف (النقر على خلفية مربع الحوار) وصورة متحركة وبعض الأحداث الإضافية لتحديد توقيت أفضل للحصول على بيانات النموذج.

إضافة رمز لإيقاف الإضاءة

هذه المهمة بسيطة وإضافة رائعة إلى عنصر مربّع حوار لا يتم إضافة تأثيرات متحركة إليه. ويحدث التفاعل من خلال مشاهدة النقرات على عنصر مربّع الحوار والاستفادة من فقاعات الأحداث لتقييم ما تم النقر عليه، ولن يتم إجراء ذلك إلا close() إذا كان هذا العنصر هو العنصر الأعلى:

export default async function (dialog) {
  dialog.addEventListener('click', lightDismiss)
}

const lightDismiss = ({target:dialog}) => {
  if (dialog.nodeName === 'DIALOG')
    dialog.close('dismiss')
}

الإشعار dialog.close('dismiss') يتم استدعاء الحدث وتوفير سلسلة. يمكن أن تسترجع JavaScript أخرى هذه السلسلة للحصول على إحصاءات عن كيفية إغلاق الحوار. ستجد أنّني قدّمت أيضًا سلاسل إغلاق في كل مرة أُطلِق فيها الدالة من أزرار مختلفة، وذلك لتوفير سياق لتطبيقي بشأن تفاعل المستخدم.

إضافة أحداث الإغلاق والأحداث المغلقة

يأتي عنصر مربّع الحوار مع حدث إغلاق: ينبعث مباشرةً عند استدعاء دالة close() في مربّع الحوار. بما أنّنا سنضيف تأثيرًا متحركًا إلى هذا العنصر، من الجميل أن يكون لدينا أحداث قبل وبعد التأثير المتحرك، وذلك لإجراء تغيير للحصول على البيانات أو إعادة ضبط نموذج مربّع الحوار. أستخدمها هنا لإدارة عملية إضافة السمة inert في مربّع الحوار المغلق، وفي العرض التوضيحي أستخدمها لتعديل قائمة الصور الرمزية في حال أرسل المستخدم صورة جديدة.

لتحقيق ذلك، أنشئ حدثَين جديدَين باسمَي closing وclosed. بعد ذلك، انتظِر حدث الإغلاق المضمّن في مربّع الحوار. من هنا، اضبط مربّع الحوار على inert وأرسِل الحدث closing. المهمة التالية هي الانتظار إلى أن تنتهي عملية تشغيل الرسوم المتحرّكة والانتقالات في مربّع الحوار، ثم إرسال الحدث closed.

const dialogClosingEvent = new Event('closing')
const dialogClosedEvent  = new Event('closed')

export default async function (dialog) {
  
  dialog.addEventListener('close', dialogClose)
}

const dialogClose = async ({target:dialog}) => {
  dialog.setAttribute('inert', '')
  dialog.dispatchEvent(dialogClosingEvent)

  await animationsComplete(dialog)

  dialog.dispatchEvent(dialogClosedEvent)
}

const animationsComplete = element =>
  Promise.allSettled(
    element.getAnimations().map(animation => 
      animation.finished))

تعرض الدالة animationsComplete، التي تُستخدَم أيضًا في إنشاء مكونات الرسائل المنبثقة ، وعدًا استنادًا إلى اكتمال وعدَي الحركة والانتقال. وهذا هو السبب في أنّ dialogClose دالة غير متزامنة، ويمكنها بعد ذلك await العودة بالوعد والانتقال بثقة إلى الحدث المغلق.

إضافة أحداث الفتح والفتح

ليس من السهل إضافة هذه الأحداث لأنّ عنصر مربّع الحوار المضمَّن لا يوفّر حدثًا مفتوحًا، كما هو الحال مع حدث الإغلاق. أستخدمُ MutationObserver لتقديم إحصاءات حول تغيير سمات مربّع الحوار. في هذا المراقب، سأراقب التغييرات التي تطرأ على السمة open وأدير الأحداث المخصصة وفقًا لذلك.

على غرار الطريقة التي بدأنا بها الأحداث المغلقة والمغلقة، أنشئ حدثَين جديدَين يُطلق عليهما opening وopened. في المكان الذي كنا ننتظر فيه سابقًا حدث إغلاق الحوار ، استخدِم هذه المرة مراقبًا تم إنشاؤه للطفرات من أجل مراقبة سمات الحوار.


const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent  = new Event('opened')

export default async function (dialog) {
  
  dialogAttrObserver.observe(dialog, { 
    attributes: true,
  })
}

const dialogAttrObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(async mutation => {
    if (mutation.attributeName === 'open') {
      const dialog = mutation.target

      const isOpen = dialog.hasAttribute('open')
      if (!isOpen) return

      dialog.removeAttribute('inert')

      // set focus
      const focusTarget = dialog.querySelector('[autofocus]')
      focusTarget
        ? focusTarget.focus()
        : dialog.querySelector('button').focus()

      dialog.dispatchEvent(dialogOpeningEvent)
      await animationsComplete(dialog)
      dialog.dispatchEvent(dialogOpenedEvent)
    }
  })
})

سيتم استدعاء دالة استدعاء مراقب التغيير عند تغيير سمات مربّع الحوار، مع توفير قائمة بالتغييرات على شكل مصفوفة. كرر الأمر فوق تغييرات السمة، وابحث عن attributeName ليتم فتحه. بعد ذلك، تحقَّق مما إذا كان العنصر يحتوي على السمة أم لا: يشير ذلك إلى ما إذا كان مربّع الحوار قد أصبح مفتوحًا أم لا. إذا تم فتحه، أزِل سمة inert، واضبط التركيز على عنصر يطلب autofocus أو أول عنصر button يتم العثور عليه في مربّع الحوار. أخيرًا، على غرار الحدث الختامي والمغلق، أرسل الحدث الافتتاحي على الفور، وانتظر انتهاء الرسوم المتحركة، ثم أرسل الحدث الافتتاحي.

إضافة حدث تمت إزالته

في تطبيقات الصفحة الواحدة، غالبًا ما تتم إضافة مربّعات الحوار وإزالتها بناءً على المسارات أو احتياجات وحالات التطبيق الأخرى. قد يكون من المفيد تنظيف الأحداث أو البيانات عند إزالة مربّع حوار.

يمكنك تحقيق ذلك باستخدام مراقب طفرات آخر. هذه المرة، بدلاً من مراقبة السمات في عنصر مربّع حوار، سنراقب العناصر الفرعية لعنصر النص ونبحث عن عناصر مربّعات الحوار التي تتم إزالتها.


const dialogRemovedEvent = new Event('removed')

export default async function (dialog) {
  
  dialogDeleteObserver.observe(document.body, {
    attributes: false,
    subtree: false,
    childList: true,
  })
}

const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(mutation => {
    mutation.removedNodes.forEach(removedNode => {
      if (removedNode.nodeName === 'DIALOG') {
        removedNode.removeEventListener('click', lightDismiss)
        removedNode.removeEventListener('close', dialogClose)
        removedNode.dispatchEvent(dialogRemovedEvent)
      }
    })
  })
})

يتمّ استدعاء دالة الاستدعاء لمُراقِب التطوّر عند إضافة عناصر فرعية أو إزالتها من نصّ المستند. إنّ الطفرات المحدّدة التي يتمّ تتبّعها هي لكلمات removedNodes التي تحتوي على nodeName مربّع حوار. في حال إزالة مربّع حوار، تتم إزالة أحداث النقر والإغلاق لمحاولة إخلاء الذاكرة، ويتم إرسال الحدث المخصّص الذي تمّت إزالته.

إزالة سمة التحميل

لمنع عرض المؤثر المرئي للخروج من مربّع الحوار عند إضافته إلى الصفحة أو عند تحميل الصفحة، تمت إضافة سمة loading إلى مربّع الحوار. ينتظر النص التالي انتهاء تشغيل الصور المتحركة لمربّع الحوار، ثم يزيل السمة. أصبح الآن بالإمكان عرض مؤثرات متحركة للحوار عند ظهوره أو اختفائه، كما تم إخفاء مؤثر متحرك كان يصرف الانتباه.

export default async function (dialog) {
  
  await animationsComplete(dialog)
  dialog.removeAttribute('loading')
}

اطّلِع على مزيد من المعلومات عن مشكلة منع الرسوم المتحرّكة للإطارات الرئيسية عند تحميل الصفحة هنا.

الكل معًا

في ما يلي القسم dialog.js بالكامل، بعد أن شرحنا كل قسم بشكلٍ فردي:

// custom events to be added to <dialog>
const dialogClosingEvent = new Event('closing')
const dialogClosedEvent  = new Event('closed')
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent  = new Event('opened')
const dialogRemovedEvent = new Event('removed')

// track opening
const dialogAttrObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(async mutation => {
    if (mutation.attributeName === 'open') {
      const dialog = mutation.target

      const isOpen = dialog.hasAttribute('open')
      if (!isOpen) return

      dialog.removeAttribute('inert')

      // set focus
      const focusTarget = dialog.querySelector('[autofocus]')
      focusTarget
        ? focusTarget.focus()
        : dialog.querySelector('button').focus()

      dialog.dispatchEvent(dialogOpeningEvent)
      await animationsComplete(dialog)
      dialog.dispatchEvent(dialogOpenedEvent)
    }
  })
})

// track deletion
const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(mutation => {
    mutation.removedNodes.forEach(removedNode => {
      if (removedNode.nodeName === 'DIALOG') {
        removedNode.removeEventListener('click', lightDismiss)
        removedNode.removeEventListener('close', dialogClose)
        removedNode.dispatchEvent(dialogRemovedEvent)
      }
    })
  })
})

// wait for all dialog animations to complete their promises
const animationsComplete = element =>
  Promise.allSettled(
    element.getAnimations().map(animation => 
      animation.finished))

// click outside the dialog handler
const lightDismiss = ({target:dialog}) => {
  if (dialog.nodeName === 'DIALOG')
    dialog.close('dismiss')
}

const dialogClose = async ({target:dialog}) => {
  dialog.setAttribute('inert', '')
  dialog.dispatchEvent(dialogClosingEvent)

  await animationsComplete(dialog)

  dialog.dispatchEvent(dialogClosedEvent)
}

// page load dialogs setup
export default async function (dialog) {
  dialog.addEventListener('click', lightDismiss)
  dialog.addEventListener('close', dialogClose)

  dialogAttrObserver.observe(dialog, { 
    attributes: true,
  })

  dialogDeleteObserver.observe(document.body, {
    attributes: false,
    subtree: false,
    childList: true,
  })

  // remove loading attribute
  // prevent page load @keyframes playing
  await animationsComplete(dialog)
  dialog.removeAttribute('loading')
}

استخدام وحدة dialog.js

تتوقّع الدالة التي تمّ تصديرها من الوحدة أن يتمّ استدعاؤها وإرسال عنصر مربّع حوار يريد إضافة هذه الأحداث والوظائف الجديدة:

import GuiDialog from './dialog.js'

const MegaDialog = document.querySelector('#MegaDialog')
const MiniDialog = document.querySelector('#MiniDialog')

GuiDialog(MegaDialog)
GuiDialog(MiniDialog)

وبهذه الطريقة، تم ترقية مربّعَي الحوار مع إصلاحات خفيفة لإغلاق مربّع الحوار ورسوم متحركة لتحميل ومزيد من الأحداث للعمل معها.

الاستماع إلى الأحداث المخصّصة الجديدة

يمكن الآن لكل عنصر محادثة تمت ترقيته الاستماع إلى خمسة أحداث جديدة، على النحو التالي:

MegaDialog.addEventListener('closing', dialogClosing)
MegaDialog.addEventListener('closed', dialogClosed)

MegaDialog.addEventListener('opening', dialogOpening)
MegaDialog.addEventListener('opened', dialogOpened)

MegaDialog.addEventListener('removed', dialogRemoved)

في ما يلي مثالان على التعامل مع هذه الأحداث:

const dialogOpening = ({target:dialog}) => {
  console.log('Dialog opening', dialog)
}

const dialogClosed = ({target:dialog}) => {
  console.log('Dialog closed', dialog)
  console.info('Dialog user action:', dialog.returnValue)

  if (dialog.returnValue === 'confirm') {
    // do stuff with the form values
    const dialogFormData = new FormData(dialog.querySelector('form'))
    console.info('Dialog form data', Object.fromEntries(dialogFormData.entries()))

    // then reset the form
    dialog.querySelector('form')?.reset()
  }
}

في العرض الترويجي الذي أنشأته باستخدام عنصر مربّع الحوار، أستخدِم هذا الحدث المغلق وبيانات النموذج لإضافة عنصر ملف شخصي جديد إلى القائمة. إنّ التوقيت مناسب، لأنّ مربّع الحوار قد أكمل الحركة المرسومة للخروج، ثمّ ظهرت بعض النصوص البرمجية التي تحرّك العناصر في الصورة الرمزية الجديدة. بفضل الأحداث الجديدة، يمكن أن يكون تنظيم تجربة المستخدم أكثر سلاسة.

إشعار dialog.returnValue: يحتوي هذا على سلسلة الإغلاق التي تم تمريرها عند استدعاء حدث مربّع الحوار close(). من المهم في حدث dialogClosed معرفة ما إذا تم إغلاق مربّع الحوار أو إلغاؤه أو تأكيده. إذا تم التأكيد، فإن النص يجلب قيم النموذج ويعيد تعيين النموذج. تكون إعادة الضبط مفيدة بحيث عندما يظهر مربع الحوار مرة أخرى، يكون فارغًا وجاهزًا لإرسال جديد.

الخاتمة

الآن بعد أن عرفت كيف فعلت ذلك، كيف ستفعل ذلك؟ 🙂

لننوّع أساليبنا ونتعرّف على جميع الطرق لإنشاء تطبيقات على الويب.

أنشئ عرضًا توضيحيًا وأرسِل إلينا رابطًا على Twitter، وسنضيفه إلى قسم الريمكسات التي أنشأها المستخدمون أدناه.

الريمكسات التي أنشأها المستخدمون

الموارد