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

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

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

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

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

نظرة عامة

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

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

دعم المتصفح

  • 37
  • 79
  • 98
  • 15.4

المصدر

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

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>

مربع حوار ضخم

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

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

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

الأنماط

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

تسريحة شعر مفتوحة

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

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

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

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

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
  • 79
  • 103
  • 9

المصدر

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

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

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

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

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

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

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

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

عادةً ما يكون 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>

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

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

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

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

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

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

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

الخلاصة

الآن بعد أن تعرّفت على كيفية إجراء ذلك، كيف يمكنك‽ 🙂

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

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

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

المراجع