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

نظرة عامة أساسية حول كيفية إنشاء مربّعات نوافذ مشروطة صغيرة وكبيرة تتكيّف مع الألوان وتتوافق مع جميع الأجهزة وتتيح إمكانية الوصول إليها باستخدام العنصر <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. كما هو موضّح سابقًا، أرى أنّه من أفضل الممارسات وضع هذا الرمز على زر الإلغاء وليس زر التأكيد. وهذا يضمن أنّه تم تقديم التأكيد عن قصد وليس عن طريق الخطأ.

الإغلاق باستخدام مفتاح Esc

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

الأنماط

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

تنسيق العناصر باستخدام Open Props

لتسريع عملية استخدام الألوان التكيُّفية وتحقيق اتساق التصميم العام، استخدمت بكل راحة مكتبة متغيّرات 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
  • ‫Edge: 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 المطوّرين التي تُظهر معلومات عن Flexbox على عناصر قائمة التذييل

Animation

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

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

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

  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;
  }
}

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

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

@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 أخرى هذه السلسلة للحصول على إحصاءات عن كيفية إغلاق الحوار. ستجد أيضًا أنّني قدّمت سلاسل إغلاق في كل مرة أُطلِق فيها الدالة من أزرار مختلفة، وذلك لتوفير سياق لتطبيقي بشأن تفاعل المستخدم.

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

يتضمّن عنصر مربّع الحوار حدث إغلاق: يتمّ بثّه على الفور عند استدعاء الدالة dialog 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

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

الخاتمة

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

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

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

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

الموارد