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

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

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

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

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

نظرة عامة

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

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

Browser Support

  • Chrome: 37.
  • Edge: 79.
  • Firefox: 98.
  • Safari: 15.4.

Source

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

Markup

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

<dialog>
  …
</dialog>

يمكننا تحسين هذا الأساس.

في العادة، يتشارك عنصر مربّع الحوار الكثير من الخصائص مع النافذة المنبثقة، وغالبًا ما يمكن استخدام الاسمين بالتبادل. لقد استخدمتُ هنا عنصر مربع الحوار لكل من النوافذ المنبثقة الصغيرة (mini) ومربعات الحوار بملء الصفحة (mega). أطلقتُ عليهما اسمَي mega وmini، مع تعديل بسيط على كلا مربّعَي الحوار ليناسبا حالات الاستخدام المختلفة. أضفتُ السمة 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 dialog

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

<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 لمراقبة تركيز العنصر، وعندها يتم اعتراض التركيز وإعادته.

Browser Support

  • Chrome: 102.
  • Edge: 102.
  • Firefox: 112.
  • Safari: 15.5.

Source

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

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

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

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

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

الأنماط

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

تحديد الأنماط باستخدام Open Props

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

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

ملكية خاصية العرض

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

dialog {
  display: grid;
}

من خلال تغيير قيمة السمة display، وبالتالي امتلاكها، كما هو موضّح في مقتطف 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 الزائف

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

Browser Support

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 103.
  • Safari: 18.

Source

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

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

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

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

ميزات إضافية لتصميم التطبيق

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

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

عندما يظهر مربّع الحوار، يظل بإمكان المستخدم تمرير الصفحة خلفه، وهذا ما لا أريده:

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

Browser Support

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

Source

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>

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

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 تعرض معلومات حول تخطيط مربّع مرن فوق عنصر التذييل

يُستخدَم العنصر menu لاحتواء أزرار الإجراءات في مربّع الحوار. يستخدم هذا الرمز تخطيطًا مرنًا مع 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 تعرض معلومات حول مربّع مرن فوق عناصر قائمة التذييل

Animation

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

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

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

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

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

على الرغم من أنّ Open Props تتضمّن إطارات مفتاحية للتلاشي والظهور، إلا أنّني أفضّل هذا النهج المتعدّد الطبقات للانتقالات كإعداد تلقائي مع إمكانية ترقية الرسوم المتحركة باستخدام الإطارات المفتاحية. لقد سبق أن حدّدنا نمط مستوى ظهور مربّع الحوار باستخدام السمة opacity، حيث يتم عرض 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;
  }
}

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

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

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

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

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

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

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


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 لمربّع حوار. إذا تمت إزالة مربّع حوار، ستتم إزالة حدثَي النقر والإغلاق لتوفير مساحة في الذاكرة، وسيتم إرسال حدث الإزالة المخصّص.

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

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

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

الخاتمة

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

لنستكشف الطرق المختلفة لإنشاء مواقع إلكترونية على الويب.

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

ريمكسات من إنشاء المنتدى

الموارد