İletişim kutusu bileşeni oluşturma

<dialog> öğesiyle renk uyarlanabilir, duyarlı ve erişilebilir mini ve mega kalıcı modlar oluşturmaya temel bir genel bakış.

Bu yayında, <dialog> öğesiyle renge uyarlanabilir, duyarlı ve erişilebilir küçük ve mega modallerin nasıl oluşturulacağıyla ilgili düşüncelerimi paylaşmak istiyorum. Demoyu deneyin ve kaynağı görüntüleyin.

Mega ve mini diyalogların açık ve koyu temalarıyla gösterimi.

Videoyu tercih ediyorsanız bu yayının YouTube sürümünü burada bulabilirsiniz:

Genel bakış

<dialog> öğesi, sayfa içi bağlamsal bilgiler veya işlemler için idealdir. Kullanıcı deneyiminin birden çok sayfalı işlem yerine aynı sayfa işleminden faydalanabileceği durumları düşünün. Bu durum, formun küçük olması veya kullanıcının yapması gereken tek işlemin onaylama ya da iptal işlemi olması olabilir.

<dialog> öğesi yakın zamanda farklı tarayıcılarda kararlı hale geldi:

Tarayıcı Desteği

  • 37
  • 79
  • 98
  • 15,4

Kaynak

Öğede birkaç şeyin eksik olduğunu fark ettim ve bu GUI Meydan Okuması'nda beklediğim geliştirici deneyimi öğelerini ekliyorum: ek etkinlikler, ışık kapatma, özel animasyonlar, mini ve mega tür.

Markup

<dialog> öğesinin temel özellikleri mütevazıdır. Öğe otomatik olarak gizlenir ve içeriğinizin yerini alacak şekilde yerleşik stillere sahiptir.

<dialog>
  …
</dialog>

Bu temel çizgiyi iyileştirebiliriz.

Geleneksel olarak, bir diyalog öğesi modal ile birçok şey paylaşır ve çoğu zaman adlar birbirinin yerine kullanılabilir. Burada diyalog öğesini hem küçük diyalog pop-up'ları (mini) hem de tam sayfa diyaloglar (mega) için kullanma özgürlüğünü çektim. Bunları mega ve mini olarak adlandırdım. Her iki iletişim kutusu da farklı kullanım alanlarına birazdan uyarlanmış. Türü belirtmenize olanak tanımak için bir modal-mode özelliği ekledim:

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

Açık ve koyu temadaki mini ve mega iletişim kutularının ekran görüntüsü.

Her zaman olmasa da, bazı etkileşim bilgilerini toplamak için genellikle iletişim öğeleri kullanılır. İletişim kutusu öğelerinin içindeki formlar bir araya gelecek şekilde oluşturulmuştur. JavaScript'in kullanıcının girdiği verilere erişebilmesi için iletişim kutusu içeriğinizi bir form öğesinin sarmalaması önerilir. Dahası, method="dialog" kullanan bir formun içindeki düğmeler JavaScript olmadan iletişim kutusunu kapatabilir ve veri aktarabilir.

<dialog id="MegaDialog" modal-mode="mega">
  <form method="dialog">
    …
    <button value="cancel">Cancel</button>
    <button value="confirm">Confirm</button>
  </form>
</dialog>

Mega iletişim kutusu

Mega iletişim kutusunun form içinde üç öğesi vardır: <header>, <article> ve <footer>. Bunlar, anlamsal kapsayıcılar ve iletişim kutusunun sunumu için stil hedefleri görevi görür. Başlık, kalıcı metin olarak adlandırılır ve bir kapat düğmesi sunar. Bu makale form girişleri ve bilgiler içindir. Alt bilgide işlem düğmelerinden oluşan bir <menu> yer bulunur.

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

İlk menü düğmesinde autofocus ve bir onclick satır içi etkinlik işleyici vardır. İletişim kutusu açıldığında autofocus özelliği odağı alır ve en iyi yöntem bunun onay düğmesine değil, iptal düğmesine yerleştirilmesidir. Bu şekilde doğrulama kasten yapılır, hatalı olmasın.

Mini iletişim kutusu

Mini iletişim kutusu, mega iletişim kutusuna çok benziyor. Sadece bir <header> öğesi eksik. Bu, reklam öğesinin daha küçük ve daha satır içi olmasını sağlar.

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

Diyalog öğesi, veri ve kullanıcı etkileşimi toplayabilen tam görüntü alanı öğesi için güçlü bir temel sağlar. Bu temel öğeler, sitenizde veya uygulamanızda oldukça ilginç ve güçlü etkileşimler sağlayabilir.

Erişilebilirlik

İletişim öğesi çok iyi bir yerleşik erişilebilirliğe sahip. Bu özellikleri her zaman yaptığım gibi eklemek yerine, birçoğu zaten orada.

Odak geri yükleniyor

Sidenav bileşeni oluşturma konusunda manuel olarak yaptığımız gibi, bir öğeyi doğru şekilde açıp kapatmanın, ilgili açma ve kapatma düğmelerine odaklanması önemlidir. Söz konusu yan gezinme çubuğu açıldığında odak, kapat düğmesine konur. Kapat düğmesine basıldığında, odak, düğmeyi açan düğmeye geri yüklenir.

İletişim öğesi kullanıldığında, yerleşik varsayılan davranıştır:

İletişim kutusunu içeri ve dışarı almak istediğinizde bu işlev kullanılamaz. JavaScript bölümünde bu işlevi geri yükleyeceğim.

Yakalama odağı

İletişim öğesi, dokümanda inert özelliğini sizin için yönetir. inert öncesinde JavaScript, odağın bir öğeden ayrılıp ayrılmadığına dikkat etmek için kullanılıyordu. Bu noktada öğe engellenip öğeyi geri koyuyor.

Tarayıcı Desteği

  • 102
  • 102
  • 112
  • 15,5

Kaynak

inert tarihinden sonra, dokümanın herhangi bir bölümü artık odak hedefi olmayacak veya fareyle etkileşimli olacak şekilde "dondurulabilir". Odağı hapsetmek yerine odak, belgenin tek etkileşimli bölümüne yönlendirilir.

Bir öğeyi açma ve otomatik odaklama

Varsayılan olarak iletişim kutusu, odağı iletişim kutusu işaretlemesindeki ilk odaklanılabilir öğeye atar. Bu, kullanıcının varsayılan olarak kullanacağı en iyi öğe değilse autofocus özelliğini kullanın. Daha önce de belirttiğim gibi bence en iyi uygulama, onay düğmesine değil, iptal düğmesine yerleştirmek. Bu, onayın kasıtlı olmasını ve kazara yapılmadığını sağlar.

Escape tuşuyla kapatma

Çalışmayı kesintiye uğratabilecek bu unsurun kolayca kapatılabilmesi önemlidir. Neyse ki iletişim öğesi, çıkış anahtarını sizin yerinize hallederek sizi düzenleme yükünden kurtarır.

Stiller

Diyalog öğesinin ve sabit yolunun stillerini belirlemenin kolay bir yolu vardır. İletişimin görüntüleme özelliğinin değiştirilmemesi ve sınırlamalarına uyulmasıyla kolay yol sağlanır. İletişim kutusunu açıp kapatmak, display özelliğini devralmak ve daha fazlasını yapmak için özel animasyonlar sağlamak üzere zor yoldan ilerliyorum.

Açık Sahnelerle Stil Oluşturma

Uyarlanabilir renkleri ve genel tasarım tutarlılığını hızlandırmak için Open Props adlı CSS değişken kitaplığımda utanç verici bir yol izledim. Ücretsiz sağlanan değişkenlere ek olarak, bir normalize dosyası ve bazı düğmeleri de içe aktarıyorum. Open Props'lar bunların her ikisini de isteğe bağlı içe aktarma işlemleri olarak sağlar. Bu içe aktarma işlemleri, iletişim kutusunu ve demoyu özelleştirmeye odaklanmama yardımcı oluyor. Ayrıca destek ve iyi görünmesini sağlamak için çok fazla stile gerek kalmıyor.

<dialog> öğesinin stilini belirleme

Görüntülü reklam mülküne sahip olma

Bir iletişim öğesinin varsayılan gösterme ve gizleme davranışı, görüntülü reklam özelliğini block değerinden none değerine değiştirir. Maalesef bu, yalnızca içeri ve dışarı çıkarılamayacağı anlamına gelir. Hem giriş hem de çıkış animasyonu yapmak istiyorum ve ilk adım kendi display özelliğimi ayarlamak:

dialog {
  display: grid;
}

Yukarıdaki CSS snippet'inde gösterildiği gibi görüntülü reklam özelliği değerini değiştirip bu değer sahibi olarak, uygun kullanıcı deneyimini kolaylaştırmak için önemli miktarda stilin yönetilmesi gerekir. İlk olarak, iletişim kutusunun varsayılan durumu kapatılır. Bu durumu görsel olarak temsil edebilir ve iletişim kutusunun aşağıdaki stillerle etkileşimler almasını engelleyebilirsiniz:

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

Artık iletişim kutusu görünmez durumda ve açık değilken iletişim kutusuyla etkileşimde bulunulamaz. Daha sonra, iletişim kutusunda inert özelliğini yönetmek için birkaç JavaScript ekleyeceğim. Böylece, klavye ve ekran okuyucu kullanıcılarının da gizli iletişim kutusuna ulaşmasını engelleyeceğim.

İletişim kutusuna uyarlanabilir bir renk teması verme

Açık ve koyu temayı gösteren, yüzey renklerini gösteren mega iletişim kutusu.

color-scheme dokümanınız için açık ve koyu sistem tercihlerine tarayıcı tarafından sağlanan uyarlanabilir bir renk temasını etkinleştirirken iletişim öğesini bundan daha fazla özelleştirmek istedim. Open Props, color-scheme kullanımına benzer şekilde açık ve koyu sistem tercihlerine otomatik olarak uyum sağlayan birkaç yüzey rengi sağlar. Bunlar, bir tasarımda katman oluşturmak için çok kullanışlı. Katman yüzeylerinin bu görünümünü görsel olarak desteklemek için renkler kullanmayı seviyorum. Arka plan rengi var(--surface-1); bu katmanın üzerine oturmak için var(--surface-2) değerini kullanın:

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

İleride üstbilgi ve altbilgi gibi alt öğeler için daha fazla uyarlanabilir renkler eklenecektir. Bunları diyalog öğesi için ekstra bir şey olarak düşünüyorum, ancak ilgi çekici ve iyi tasarlanmış bir diyalog tasarımında gerçekten çok önemli.

Duyarlı iletişim kutusu boyutlandırma

İletişim kutusu, varsayılan olarak boyutu içeriğine yetki verir. Bu da genellikle harika bir yöntemdir. Buradaki amacım max-inline-size alanını okunabilir bir boyutla (--size-content-3 = 60ch) veya görüntü alanı genişliğinin% 90'ını sınırlamak. Bu, iletişimin mobil cihazlarda uçtan uca kaymamasını ve masaüstü ekranında okumak zor olacak kadar geniş olmamasını sağlar. Ardından, iletişim kutusunun sayfanın yüksekliğini aşmaması için bir max-block-size ekliyorum. Bu, uzun bir iletişim kutusu öğesi olması durumunda iletişim kutusunun kaydırılabilir alanının nerede olduğunu da belirtmemiz gerektiği anlamına gelir.

dialog {
  …
  max-inline-size: min(90vw, var(--size-content-3));
  max-block-size: min(80vh, 100%);
  max-block-size: min(80dvb, 100%);
  overflow: hidden;
}

İki kez max-block-size işareti yaptığımı fark ettin mi? Birincisi, fiziksel bir görüntü alanı birimi olan 80vh'i kullanır. Gerçekten istediğim şey, iletişim kutusunun uluslararası kullanıcılar için göreli akış içinde kalmasını sağlamak. Bu yüzden, daha istikrarlı hale geleceği zaman için ikinci bildirimde mantıksal, daha yeni ve yalnızca kısmen desteklenen dvb birimini kullanıyorum.

Mega diyalog konumlandırması

Bir iletişim öğesinin yerleştirilmesine yardımcı olmak için, tam ekran arka planı ve iletişim kutusu kapsayıcısı olmak üzere iki parçasını ayırmak yararlı olacaktır. Arka plan, bu iletişim kutusunun önde olduğunu ve arkasındaki içeriğe erişilemediğini desteklemek için bir gölge efekti sağlayarak her şeyi kapsamalıdır. İletişim kutusu kapsayıcısı, kendini bu arka planın üzerinde ortalayabilir ve içeriği gereken şekli alabilir.

Aşağıdaki stiller, iletişim öğesini pencereye sabitleyerek her bir köşeye uzatır ve içeriği ortalamak için margin: auto kullanır:

dialog {
  …
  margin: auto;
  padding: 0;
  position: fixed;
  inset: 0;
  z-index: var(--layer-important);
}
Mobil mega iletişim kutusu stilleri

Küçük görüntü alanlarında, bu tam sayfa mega kalıcı iletişimin stilini biraz farklı bir şekilde değiştiriyorum. Alt kenar boşluğunu 0 olarak ayarladım. Böylece iletişim kutusu içeriği görüntü alanının en altına taşınır. Birkaç stil ayarlamasıyla iletişim kutusunu kullanıcının parmağına yakın bir eylem sayfasına dönüştürebilirim:

@media (max-width: 768px) {
  dialog[modal-mode="mega"] {
    margin-block-end: 0;
    border-end-end-radius: 0;
    border-end-start-radius: 0;
  }
}

Geliştirici araçları, açıkken hem masaüstü hem de mobil mega iletişim kutusunda kenar boşluğu aralığını kaplayan ekran görüntüsü.

Mini diyalog konumlandırması

Masaüstü bilgisayar gibi daha büyük bir görüntü alanı kullanırken, mini iletişim kutularını bunları çağıran öğenin üzerine yerleştirmeyi seçtim. Bunun için JavaScript'e ihtiyacım var. Kullandığım tekniği burada bulabilirsiniz, ancak bunun bu makalenin kapsamı dışında olduğunu düşünüyorum. JavaScript olmadan, mini iletişim kutusu tıpkı mega iletişim kutusu gibi ekranın ortasında görünür.

Çarpıcı hale getirin

Son olarak, iletişim kutusunu, sayfanın çok yukarısında yumuşak bir yüzey gibi görünür bir hale getirin. İletişimin yumuşaklığı, iletişim kutusunun köşeleri yuvarlanarak elde edilir. Derinlik, Open Props'un dikkatlice hazırlanmış gölge bileşenlerinden biriyle elde edilir:

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

Arka plan sözde öğesini özelleştirme

Arka planla çok hafif çalışmayı seçtim, yalnızca mega iletişim kutusuna backdrop-filter ile bulanıklaştırma efekti ekledim:

Tarayıcı Desteği

  • 76
  • 17
  • 103
  • 9

Kaynak

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

Ayrıca, tarayıcıların gelecekte arka plan öğesi geçişinin yapılmasına izin vereceğini umarak backdrop-filter uygulamasında bir geçiş yapmayı da tercih ettik:

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

Renkli avatarların bulunduğu bulanık bir arka planla kaplayan mega iletişimin ekran görüntüsü.

Ek özelliklerin stil özelliklerini ayarlama

Bu bölüme "ekstralar" adını veriyorum çünkü genel olarak diyalog öğesi demomdan ziyade iletişim öğe demomla alakalı.

Kaydırma kapsama

İletişim kutusu görüntülendiğinde kullanıcı hâlâ sayfanın arkasındaki sayfayı kaydırabiliyor. Ben bunu istemiyorum:

Normalde overscroll-behavior benim her zamanki çözümüm olurdu, ancak belirtilemeye göre, bir kaydırma bağlantı noktası olmadığından iletişim kutusu üzerinde hiçbir etkisi yoktur. Yani bir kaydırma çubuğu olmadığından engellenecek bir şey de yoktur. Bu kılavuzdaki "closed" ve "opened" gibi yeni etkinlikleri izlemek için JavaScript kullanabilir ve dokümanda overflow: hidden ayarını açık duruma getirebilirim veya :has() öğesinin tüm tarayıcılarda kararlı olmasını bekleyebilirim:

Tarayıcı Desteği

  • 105
  • 105
  • 121
  • 15,4

Kaynak

html:has(dialog[open][modal-mode="mega"]) {
  overflow: hidden;
}

Artık bir mega iletişim kutusu açıkken html belgesinde overflow: hidden var.

<form> düzeni

Etkileşim bilgilerini kullanıcıdan toplamak için çok önemli bir öğe olmasının yanı sıra, burada üstbilgi, altbilgi ve makale öğelerini düzenlemek için kullanıyorum. Bu düzenle makale alt öğesini kaydırılabilir bir alan olarak belirtmeyi amaçlıyorum. Bunu grid-template-rows ile elde ediyorum. Makale öğesine 1fr verilmiş ve formun kendisi iletişim öğesiyle aynı maksimum yüksekliğe sahip. Bu firma yüksekliğini ve firma satır boyutunun ayarlanması, makale öğesinin sınırlanmasına ve taştığında kaydırma yapmasına olanak tanır:

dialog > form {
  display: grid;
  grid-template-rows: auto 1fr auto;
  align-items: start;
  max-block-size: 80vh;
  max-block-size: 80dvb;
}

Devtools&#39;da satırların üzerine ızgara düzeni bilgilerini yerleştiren ekran görüntüsü.

<header> iletişim kutusunun stilini belirleme

Bu öğenin rolü, iletişim kutusu içeriği için bir başlık ve kolayca bulunabilen bir kapat düğmesi sunmaktır. Ayrıca, iletişim kutusu makale içeriğinin arkasındaymış gibi bir yüzey rengi de verilir. Bu gereksinimler bir flexbox kapsayıcısına, kenarlarına yerleştirilmiş dikey olarak hizalanmış öğelere, başlık ve kapat düğmelerine biraz alan vermek için bir miktar dolgu ve boşluklara yol açar:

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&#39;un iletişim kutusu üstbilgisine flexbox düzen bilgilerini yerleştirdiği ekran görüntüsü.

Başlığı kapatma düğmesi stilini belirleme

Demo, Open Props düğmelerini kullandığından kapat düğmesi şu şekilde yuvarlak simge merkezli bir düğme şeklinde özelleştirilir:

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&#39;un başlığı kapatma düğmesi için yer paylaşımlı boyut ve dolgu bilgilerini gösteren ekran görüntüsü.

<article> iletişim kutusunun stilini belirleme

Makale öğesinin bu iletişim kutusunda özel bir rolü vardır: Uzun veya uzun bir iletişim kutusu durumunda kaydırılması amaçlanan bir alandır.

Bunu başarmak için, üst form öğesi kendi kendine bazı maksimum değerler belirlemiştir. Bu değerler, öğe çok uzarsa ulaşılması için kısıtlamalar oluşturur. overflow-y: auto öğesini, kaydırma çubuklarının yalnızca gerektiğinde gösterileceği şekilde ayarlayın. Kaydırma çubuklarının içinde overscroll-behavior: contain ile kaydırma yapılmasını sağlayın. Geri kalanlar özel sunu stilleri olacaktır:

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

Altbilginin rolü, işlem düğmelerinin menülerini içermektir. Flexbox, içeriği altbilgi satır içi ekseninin sonuna hizalamak ve ardından düğmelere yer açmak için biraz boşluk kullanılır.

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&#39;un altbilgi öğesinde flexbox düzen bilgilerini kaplamasını gösteren ekran görüntüsü.

menu öğesi, iletişim kutusunun işlem düğmelerini eklemek için kullanılır. Düğmeler arasında boşluk bırakmak için gap ile sarmalama flexbox düzenini kullanır. Menü öğelerinde <ul> gibi dolgu bulunur. Ayrıca, ihtiyacım olmadığı için bu stili kaldırıyorum.

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&#39;un altbilgi menüsü öğeleri üzerine flexbox bilgilerini yerleştirdiğini gösteren ekran görüntüsü.

Animasyonlar

İletişim kutusu öğeleri, pencereye girip çıktıkları için genellikle canlandırılır. Diyaloglara bu giriş ve çıkışla ilgili destekleyici hareketler eklemek, kullanıcıların kendilerini akışa yönlendirmesine yardımcı olur.

Normalde iletişim kutusu yalnızca içinde canlandırılabilir, dışarı çıkmaz. Bunun nedeni, tarayıcının öğedeki display özelliğini açıp kapatmasıdır. Daha önce kılavuz, ekranı ızgara olarak ayarlar ve hiçbir zaman "yok" olarak ayarlamaz. Bu sayede içeride ve dışarıya animasyonlar eklenebilir.

Open Props, düzenlemeyi kolay ve okunabilir hale getiren birçok animasyon karesi animasyonu ile birlikte gelir. Benim kullandığım animasyon hedefleri ve katmanlı yaklaşımı şöyle:

  1. Azaltılmış hareket, varsayılan geçiştir; basit bir opaklık belirir ve kaybolur.
  2. Hareket uygunsa kaydırma ve ölçek animasyonları eklenir.
  3. Mega iletişim kutusunun duyarlı mobil düzeni, dışarı kayacak şekilde ayarlanmıştır.

Güvenli ve anlamlı bir varsayılan geçiş

Open Props'ta belirme ve kaybolma için animasyon kareleri sağlansa da, bu katmanlı geçiş yaklaşımını animasyon karesi animasyonlarıyla varsayılan olarak kullanmayı tercih ediyorum. İletişim kutusunun görünürlüğünü opaklık ile önceden biçimlendirmiştik. [open] özelliğine bağlı olarak 1 veya 0 öğelerini düzenledik. %0 ile %100 arasında geçiş için tarayıcıya ne kadar süre ve ne tür bir yumuşatma istediğinizi söyleyin:

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

Geçişe hareket ekleme

Kullanıcının hareket konusunda bir sıkıntısı yoksa hem mega hem de mini iletişim kutuları giriş olarak yukarı kaymalı ve çıkış olarak ölçek genişletilmelidir. Bunu prefers-reduced-motion medya sorgusu ve birkaç Open Props ile yapabilirsiniz:

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

Çıkış animasyonunu mobil için uyarlama

Stil bölümünün önceki bölümlerinde mobil cihazlar için uyarlanan mega diyalog stili, sanki ekranın altından yukarı doğru kayan ve hâlâ alta bir sabitlenmiş gibi bir işlem sayfasına benzer. Ölçeklenen çıkış animasyonu, bu yeni tasarıma pek uymuyor. Bunu birkaç medya sorgusu ve bazı Open Props ile uyarlayabiliriz:

@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 ile eklenecek çok sayıda şey vardır:

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

Bu eklemeler ışığın kapatılması isteğinden (iletişim kutusu arka planını tıklama), animasyondan ve form verilerini almanın daha iyi zamanlanması için bazı ek etkinliklerden kaynaklanmaktadır.

Işık kapatma özelliği ekleniyor

Bu görev basittir ve animasyonlu olmayan bir iletişim öğesi için mükemmel bir katkı sağlar. Etkileşim, iletişim öğesi tıklamaları izlenerek ve tıklanan öğeyi değerlendirmek için etkinlik baloncuklarından yararlanılarak gerçekleştirilir ve yalnızca en üstteki öğe olduğunda close() işlemi gerçekleşir:

export default async function (dialog) {
  dialog.addEventListener('click', lightDismiss)
}

const lightDismiss = ({target:dialog}) => {
  if (dialog.nodeName === 'DIALOG')
    dialog.close('dismiss')
}

Bildirim dialog.close('dismiss'). Etkinlik çağrılır ve bir dize sağlanır. Bu dize, iletişim kutusunun nasıl kapatıldığına dair bilgi almak için diğer JavaScript'ler tarafından alınabilir. Ayrıca, uygulamama kullanıcı etkileşimi hakkında bağlam sağlamak için işlevi çeşitli düğmelerden her çağırdığımda yakın dizeler sağladım.

Kapanış ve kapanış etkinlikleri ekleme

İletişim öğesi bir kapatma etkinliğiyle gelir: İletişim kutusu close() işlevi çağrıldığı anda yayılır. Bu öğeye animasyon uyguladığımızdan, animasyondan önceki ve sonraki etkinliklerin verilerini almak veya iletişim kutusunu sıfırlamak için bir değişiklik yapılması yararlı olur. Bunu kapalı iletişim kutusuna eklenen inert özelliğini yönetmek için kullanıyorum. Kullanıcı yeni bir resim gönderdiğinde ise demoda avatar listesini değiştirmek için bu özelliği kullanıyorum.

Bunun için closing ve closed adında iki yeni etkinlik oluşturun. Daha sonra, iletişim kutusunda yerleşik kapatma etkinliğini dinleyin. Buradan iletişim kutusunu inert olarak ayarlayıp closing etkinliğini dağıtın. Bir sonraki görev, animasyonların ve geçişlerin iletişim kutusundaki çalışmasının tamamlanmasını bekleyip closed etkinliğini dağıtmaktır.

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

Durum hata bildirimi bileşeni oluşturma'da da kullanılan animationsComplete işlevi, animasyon ve geçiş vaatlerinin tamamlanmasına dayalı bir söz döndürür. Bu nedenle dialogClose, eş zamansız işlev olur. Ardından, sözü await döndürüp güvenle kapalı etkinliğe devam edebilir.

Açılış ve açılan etkinlik ekleme

Yerleşik iletişim kutusu öğesi kapatmada olduğu gibi açık bir etkinlik sağlamadığından bu etkinlikleri eklemek kolay değildir. İletişim kutusunun özniteliklerinin değişimi hakkında detaylı bilgi sağlamak için MutationObserver kullanıyorum. Bu gözlemcide, açık özellikte yapılan değişiklikleri izler ve özel etkinlikleri buna göre yönetirim.

Kapanış ve kapanış etkinliklerini başlattığımıza benzer şekilde, opening ve opened adında iki yeni etkinlik oluşturun. Daha önce iletişim kutusu kapatma etkinliğini dinlediğimiz yerde, bu kez iletişim kutusunun özelliklerini izlemek için oluşturulmuş bir mutasyon gözlemleyicisi kullanılır.

…
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)
    }
  })
})

İletişim kutusu özellikleri değiştirildiğinde mutasyon gözlemleyici geri çağırma işlevi çağrılır ve değişikliklerin listesi bir dizi olarak sunulur. Özellik değişikliklerini tekrarlayarak attributeName öğesinin açık olup olmadığını kontrol edin. Daha sonra, öğenin özelliğe sahip olup olmadığını kontrol edin: Bu bilgi, iletişim kutusunun açılıp açılmadığını bildirir. Açılmışsa inert özelliğini kaldırın ve odağı, autofocus isteğinde bulunan bir öğeye veya iletişim kutusunda bulunan ilk button öğesine ayarlayın. Son olarak, kapatma ve kapatma etkinliğine benzer şekilde, açılış etkinliğini hemen dağıtın, animasyonların tamamlanmasını bekleyin, ardından açılan etkinliği dağıtın.

Kaldırılan bir etkinliği ekleme

Tek sayfalık uygulamalarda iletişim kutuları genellikle rotalara veya diğer uygulama ihtiyaçlarına ve durumuna göre eklenir ve kaldırılır. Bir iletişim kutusu kaldırıldığında etkinliklerin veya verilerin temizlenmesi faydalı olabilir.

Bunu başka bir mutasyon gözlemleyiciyle yapabilirsiniz. Bu kez, bir iletişim öğesindeki özellikleri gözlemlemek yerine, gövde öğesinin alt öğelerini gözlemleyip iletişim öğelerinin kaldırılmasını izleyeceğiz.

…
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)
      }
    })
  })
})

Belgenin gövdesinden alt öğeler eklendiğinde veya kaldırıldığında mutasyon gözlemleyici geri çağırması çağrılır. İzlenen belirli mutasyonlar, nodeName iletişim kutusuna sahip olan removedNodes içindir. Bir iletişim kutusu kaldırıldıysa bellekte yer açmak için tıklama ve kapatma etkinlikleri kaldırılır ve özel kaldırılan etkinlik gönderilir.

Yükleme özelliğini kaldırma

İletişim kutusu animasyonunun sayfaya eklendiğinde veya sayfa yüklenirken çıkış animasyonunu oynatmasını önlemek için iletişim kutusuna bir yükleme özelliği eklenmiştir. Aşağıdaki komut dosyası, iletişim kutusu animasyonlarının tamamlanmasını bekler ve daha sonra özelliği kaldırır. Artık iletişim kutusunda animasyon eklemek ve çıkış yapmak serbesttir. Dikkat dağıtıcı bir animasyonu etkili bir şekilde gizledik.

export default async function (dialog) {
  …
  await animationsComplete(dialog)
  dialog.removeAttribute('loading')
}

Sayfa yüklenirken animasyon karesi animasyonlarını engelleme sorunu hakkında daha fazla bilgiyi buradan edinebilirsiniz.

Hepsi bir arada

Her bölümü tek tek açıkladığımıza göre dialog.js öğesinin tamamını aşağıda görebilirsiniz:

// 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 modülünü kullanma

Modülden dışa aktarılan işlevin çağrılması ve bu yeni etkinliklerin ve işlevlerin eklenmesini isteyen bir iletişim öğesi geçirilmesi beklenir:

import GuiDialog from './dialog.js'

const MegaDialog = document.querySelector('#MegaDialog')
const MiniDialog = document.querySelector('#MiniDialog')

GuiDialog(MegaDialog)
GuiDialog(MiniDialog)

Bu şekilde, iki iletişim kutusu ışık kapatma, animasyon yükleme düzeltmeleri ve çalışabilecek daha fazla etkinlikle yeni sürüme geçirildi.

Yeni özel etkinlikleri dinleme

Yeni sürüme geçirilen her iletişim öğesi artık aşağıdaki gibi beş yeni etkinliği dinleyebilir:

MegaDialog.addEventListener('closing', dialogClosing)
MegaDialog.addEventListener('closed', dialogClosed)

MegaDialog.addEventListener('opening', dialogOpening)
MegaDialog.addEventListener('opened', dialogOpened)

MegaDialog.addEventListener('removed', dialogRemoved)

Aşağıda, bu etkinliklerin ele alınmasına dair iki örnek verilmiştir:

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()
  }
}

İletişim öğesiyle oluşturduğum demoda, listeye yeni bir avatar öğesi eklemek için bu kapalı etkinliği ve form verilerini kullanıyorum. Zamanlama, iletişim kutusunun çıkış animasyonunu tamamladığı ve ardından bazı komut dosyaları yeni avatarla canlandırıldığı için iyidir. Yeni etkinlikler sayesinde kullanıcı deneyimini daha sorunsuz şekilde düzenleyebilirsiniz.

Uyarı dialog.returnValue: Bu, close() iletişim kutusu etkinliği çağrıldığında iletilen kapatma dizesini içerir. dialogClosed etkinliğinde iletişimin kapatılıp kapatılmadığını, iptal edilip edilmediğini veya onaylanıp onaylanmadığını öğrenmek çok önemlidir. Onaylanırsa komut dosyası, form değerlerini alır ve formu sıfırlar. Sıfırlama işlemi, iletişim kutusu tekrar gösterildiğinde boş ve yeni bir gönderim için hazır olması açısından yararlıdır.

Sonuç

Nasıl yaptığımı artık bildiğine göre siz de nasıl yapardınız? 🙂

Yaklaşımlarımızı çeşitlendirelim ve web'de geliştirme yapmanın tüm yollarını öğrenelim.

Bir demo oluşturun, bana tweet atın bağlantıları, aşağıdaki topluluk remiksleri bölümüne ekleyeceğim.

Topluluk remiksleri

Kaynaklar