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

نظرة عامة أساسية حول طريقة تصميم نماذج مصغّرة وسريعة وسريعة وسريعة الاستجابة وسريعة الاستجابة ومناسبة للألوان باستخدام العنصر <dialog>

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

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

في ما يلي إصدار YouTube من هذه المشاركة إذا كنت تفضّل الفيديوهات:

نظرة عامة

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

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

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

  • 37
  • 79
  • 98
  • 15.4

المصدر

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

Markup

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

<dialog>
  …
</dialog>

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

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

مربّع حوار ضخم

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

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

  • 102
  • 102
  • 112
  • 15.5

المصدر

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

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

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

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

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

الأنماط

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

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

  • 76
  • 17
  • 103
  • 9

المصدر

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

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

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

لقطة شاشة لمربّع حوار ضخم يظهر على خلفية مموَّهة لصور رمزية ملوّنة.

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

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

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

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

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

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

  • 105
  • 105
  • 121
  • 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 Devtools التي تظهر على سطح معلومات تنسيق 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 Devtools

تصميم مربع الحوار <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 تظهر على سطح معلومات تصميم 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. يتم تعديل تخطيط الهاتف المحمول سريع الاستجابة لمربع الحوار الضخم بحيث يتم تمريره للخارج.

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

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

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

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

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

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

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

جارٍ إزالة سمة التحميل

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

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

الخلاصة

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

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

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

ريمكسات من المنتدى

المراجِع