ساخت مولفه گفتگو

یک نمای کلی از نحوه ساخت مینی و مگا مدال های سازگار با رنگ، پاسخگو و در دسترس با عنصر <dialog> .

در این پست می‌خواهم نظرات خود را در مورد نحوه ساخت مینی و مگا مدال‌های سازگار با رنگ، واکنش‌گرا و در دسترس با عنصر <dialog> به اشتراک بگذارم. نسخه ی نمایشی را امتحان کنید و منبع را مشاهده کنید !

نمایش دیالوگ های مگا و مینی در تم های روشن و تاریک آنها.

اگر ویدیو را ترجیح می دهید، در اینجا یک نسخه YouTube از این پست وجود دارد:

نمای کلی

عنصر <dialog> برای اطلاعات یا اقدامات متنی درون صفحه عالی است. زمانی را در نظر بگیرید که تجربه کاربر می تواند از یک عملکرد صفحه به جای عملکرد چند صفحه ای بهره مند شود: شاید به این دلیل که فرم کوچک است یا تنها اقدام مورد نیاز کاربر تأیید یا لغو است.

عنصر <dialog> اخیراً در بین مرورگرها پایدار شده است:

پشتیبانی مرورگر

  • کروم: 37.
  • لبه: 79.
  • فایرفاکس: 98.
  • سافاری: 15.4.

منبع

متوجه شدم که این عنصر چند چیز را از دست داده است، بنابراین در این چالش رابط کاربری گرافیکی، مواردی را که انتظار دارم تجربه توسعه دهندگان را اضافه می‌کنم: رویدادهای اضافی، حذف نور، انیمیشن‌های سفارشی، و نوع کوچک و مگا.

نشانه گذاری

ملزومات عنصر <dialog> بسیار کم است. این عنصر به طور خودکار پنهان می شود و دارای سبک هایی برای همپوشانی محتوای شما است.

<dialog>
  …
</dialog>

ما می توانیم این پایه را بهبود بخشیم.

به طور سنتی، یک عنصر گفتگو با یک مودال اشتراک‌گذاری زیادی دارد و اغلب نام‌ها قابل تعویض هستند. من در اینجا آزادی استفاده از عنصر دیالوگ را برای پنجره های کوچک گفتگو (مینی)، و همچنین دیالوگ های تمام صفحه (مگا) گرفتم. من آنها را مگا و مینی نامیدم، با هر دو دیالوگ کمی برای موارد استفاده متفاوت. من یک ویژگی modal-mode اضافه کردم تا بتوانید نوع آن را مشخص کنید:

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

اسکرین شات از دیالوگ های کوچک و مگا در هر دو تم روشن و تاریک.

نه همیشه، اما به طور کلی از عناصر گفتگو برای جمع آوری برخی از اطلاعات تعامل استفاده می شود. فرم‌ها در عناصر محاوره‌ای ساخته شده‌اند تا با هم باشند . ایده خوبی است که یک عنصر فرم محتوای گفتگوی خود را بپیچد تا جاوا اسکریپت بتواند به داده هایی که کاربر وارد کرده است دسترسی داشته باشد. علاوه بر این، دکمه‌های داخل فرم با استفاده از method="dialog" می‌توانند یک گفتگو را بدون جاوا اسکریپت ببندند و داده‌ها را ارسال کنند.

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

عنصر محاوره ای پایه محکمی برای یک عنصر نمای کامل فراهم می کند که می تواند داده ها و تعامل کاربر را جمع آوری کند. این موارد ضروری می توانند تعاملات بسیار جالب و قدرتمندی را در سایت یا برنامه شما ایجاد کنند.

قابلیت دسترسی

عنصر گفتگو دارای دسترسی داخلی بسیار خوبی است. به جای اضافه کردن این ویژگی ها مانند من معمولاً، بسیاری از آنها در حال حاضر وجود دارند.

بازیابی تمرکز

همانطور که در ساخت کامپوننت sidenav به صورت دستی انجام دادیم، مهم است که باز و بسته کردن چیزی به درستی روی دکمه های باز و بسته مربوطه تمرکز کند. وقتی آن نوار کناری باز می شود، روی دکمه بستن تمرکز می شود. هنگامی که دکمه بستن فشار داده می شود، فوکوس به دکمه ای باز می گردد که آن را باز کرده است.

با عنصر گفتگو، این یک رفتار پیش فرض داخلی است:

متأسفانه، اگر می خواهید گفتگو را به داخل و خارج متحرک کنید، این قابلیت از بین می رود. در بخش جاوا اسکریپت، من آن عملکرد را بازیابی خواهم کرد.

به دام انداختن تمرکز

عنصر محاوره ای inert را برای شما در سند مدیریت می کند. قبل از inert ، جاوا اسکریپت برای تماشای فوکوس ترک یک عنصر استفاده می‌شد، در این مرحله آن را قطع می‌کند و آن را برمی‌گرداند.

پشتیبانی مرورگر

  • کروم: 102.
  • لبه: 102.
  • فایرفاکس: 112.
  • سافاری: 15.5.

منبع

پس از inert ، هر بخش از سند را می توان به گونه ای "منجمد" کرد که دیگر هدف تمرکز نباشد یا با ماوس تعامل داشته باشد. به جای به دام انداختن تمرکز، تمرکز به تنها بخش تعاملی سند هدایت می شود.

باز کردن و فوکوس خودکار یک عنصر

به طور پیش‌فرض، عنصر محاوره‌ای فوکوس را به اولین عنصر قابل فوکوس در نشانه‌گذاری گفتگو اختصاص می‌دهد. اگر این بهترین عنصری نیست که کاربر به صورت پیش‌فرض آن را انتخاب کند، از ویژگی autofocus استفاده کنید. همانطور که قبلاً توضیح داده شد، به نظر من بهترین تمرین این است که این را روی دکمه لغو قرار دهید و نه دکمه تأیید. این تضمین می کند که تأیید عمدی است و تصادفی نیست.

بسته شدن با کلید فرار

مهم است که بستن این عنصر بالقوه مزاحم را آسان کنید. خوشبختانه، عنصر گفتگو کلید فرار را برای شما کنترل می کند و شما را از بار ارکستراسیون رها می کند.

سبک ها

یک مسیر آسان برای طراحی عنصر گفتگو و یک مسیر سخت وجود دارد. مسیر آسان با تغییر نکردن ویژگی نمایش دیالوگ و کار با محدودیت های آن به دست می آید. من مسیر سختی را طی می‌کنم تا انیمیشن‌های سفارشی برای باز کردن و بستن دیالوگ، تصاحب ویژگی display و موارد دیگر ارائه کنم.

یک ظاهر طراحی با ابزارهای باز

برای سرعت بخشیدن به رنگ‌های تطبیقی ​​و هماهنگی کلی طراحی، بی‌شرمانه کتابخانه متغیر CSS خود را Open Props آورده‌ام. علاوه بر متغیرهای رایگان ارائه شده، من یک فایل عادی و چند دکمه را نیز وارد می‌کنم، که Open Props هر دو را به عنوان واردات اختیاری ارائه می‌کند. این واردات به من کمک می‌کند تا روی سفارشی‌سازی دیالوگ و نسخه نمایشی تمرکز کنم، در حالی که نیازی به استایل‌های زیادی برای پشتیبانی از آن و خوب جلوه دادن آن ندارم.

استایل دادن به عنصر <dialog>

مالکیت نمایشگر

رفتار نمایش و پنهان کردن پیش‌فرض یک عنصر گفتگو، ویژگی نمایش را از block به none تغییر می‌دهد. متأسفانه به این معنی است که نمی‌توان آن را در داخل و خارج متحرک کرد، فقط به داخل .

dialog {
  display: grid;
}

همانطور که در قطعه CSS بالا نشان داده شده است، با تغییر، و در نتیجه مالکیت، ارزش ویژگی نمایشگر، مقدار قابل توجهی از سبک ها به منظور تسهیل تجربه کاربری مناسب نیاز دارند. ابتدا حالت پیش فرض یک دیالوگ بسته می شود. می توانید این حالت را به صورت بصری نشان دهید و از دریافت تعامل با استایل های زیر در گفتگو جلوگیری کنید:

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

اکنون دیالوگ نامرئی است و وقتی باز نیست نمی توان با آن تعامل کرد. بعداً مقداری جاوا اسکریپت را برای مدیریت ویژگی 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;
  }
}

تصویری از ابزارهای توسعه‌یافته که فاصله حاشیه‌ها را پوشانده‌اند    در مگا دیالوگ دسکتاپ و موبایل در حالی که باز است.

موقعیت یابی مینی دیالوگ

هنگام استفاده از یک نمای بزرگتر مانند یک رایانه رومیزی، من انتخاب کردم که دیالوگ های کوچک را روی عنصری که آنها را فراخوانی می کند قرار دهم. برای انجام این کار به جاوا اسکریپت نیاز دارم. می‌توانید تکنیکی را که من استفاده می‌کنم در اینجا بیابید ، اما احساس می‌کنم فراتر از محدوده این مقاله است. بدون جاوا اسکریپت، دیالوگ کوچک در مرکز صفحه ظاهر می شود، درست مانند مگا دیالوگ.

آن را پاپ کنید

در آخر، کمی استعداد به گفتگو اضافه کنید تا مانند سطح نرمی که در بالای صفحه قرار دارد به نظر برسد. نرمی با گرد کردن گوشه های دیالوگ به دست می آید. عمق با یکی از سایه بان های Open Props که با دقت ساخته شده اند به دست می آید:

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

سفارشی کردن عنصر شبه پس زمینه

من تصمیم گرفتم خیلی سبک با پس‌زمینه کار کنم، فقط یک جلوه تاری با backdrop-filter به گفتگوی مگا اضافه کردم:

پشتیبانی مرورگر

  • کروم: 76.
  • لبه: 79.
  • فایرفاکس: 103.
  • سافاری: 18.

منبع

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

من همچنین انتخاب کردم که یک انتقال در backdrop-filter قرار دهم، به این امید که مرورگرها اجازه انتقال عنصر پس‌زمینه را در آینده بدهند:

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

اسکرین شات از دیالوگ بزرگ که پس‌زمینه‌ای تار از آواتارهای رنگارنگ را پوشانده است.

موارد اضافی استایل

من این بخش را "اضافی" می نامم زیرا بیشتر با عنصر دیالوگ نمایشی من ارتباط دارد تا عنصر گفتگو به طور کلی.

محتویات اسکرول

وقتی دیالوگ نشان داده می شود، کاربر همچنان می تواند صفحه پشت آن را اسکرول کند، که من نمی خواهم:

به طور معمول، overscroll-behavior راه حل معمول من خواهد بود، اما با توجه به مشخصات ، هیچ تاثیری در گفتگو ندارد زیرا یک پورت اسکرول نیست، یعنی یک اسکرول نیست، بنابراین چیزی برای جلوگیری از آن وجود ندارد. می‌توانم از جاوا اسکریپت برای تماشای رویدادهای جدید از این راهنما استفاده کنم، مانند "بسته" و "باز"، و 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 Devtools که اطلاعات چیدمان flexbox را روی عنصر پاورقی می پوشاند.

عنصر 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 Devtools که اطلاعات flexbox را روی عناصر منوی پاورقی می پوشاند.

انیمیشن

عناصر دیالوگ اغلب متحرک هستند زیرا وارد پنجره می شوند و از آن خارج می شوند. دادن چند حرکت حمایتی به دیالوگ ها برای این ورودی و خروجی به کاربران کمک می کند تا خود را در جریان جهت دهی کنند.

معمولاً عنصر گفتگو فقط می تواند در داخل متحرک باشد، نه خارج. این به این دلیل است که مرورگر ویژگی 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);
  }
}

جاوا اسکریپت

چیزهای زیادی برای اضافه کردن با جاوا اسکریپت وجود دارد:

// 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') توجه کنید. رویداد فراخوانی می شود و یک رشته ارائه می شود. این رشته می تواند توسط جاوا اسکریپت های دیگر بازیابی شود تا بینش هایی در مورد نحوه بسته شدن گفتگو بدست آید. متوجه خواهید شد که هر بار که تابع را از دکمه‌های مختلف فراخوانی می‌کنم، رشته‌های نزدیک را نیز ارائه کرده‌ام تا زمینه را برای برنامه‌ام در مورد تعامل کاربر فراهم کنم.

افزودن رویدادهای بسته و بسته

عنصر محاوره ای با یک رویداد بسته می آید: هنگامی که تابع 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 که در کامپوننت Building a toast نیز استفاده می‌شود، یک وعده مبتنی بر تکمیل انیمیشن و وعده‌های انتقال برمی‌گرداند. به همین دلیل است که 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() dialog ارسال می شود. در رویداد dialogClosed بسیار مهم است که بدانید آیا گفتگو بسته، لغو یا تأیید شده است. اگر تایید شد، اسکریپت سپس مقادیر فرم را می گیرد و فرم را بازنشانی می کند. بازنشانی مفید است به طوری که وقتی دیالوگ دوباره نشان داده شد، خالی و آماده برای ارسال جدید است.

نتیجه گیری

حالا که می دانید من چگونه این کار را انجام دادم، چگونه این کار را انجام می دهید‽🙂

بیایید رویکردهایمان را متنوع کنیم و همه راه‌های ساخت در وب را بیاموزیم.

یک نسخه نمایشی ایجاد کنید، پیوندها را برای من توییت کنید ، و من آن را به بخش ریمیکس های انجمن در زیر اضافه می کنم!

ریمیکس های انجمن

منابع