Codelab: Hikayeler bileşeni oluşturma

Bu codelab'de, web'de Instagram Hikayeleri gibi bir deneyimin nasıl oluşturulacağı açıklanmaktadır. HTML, CSS ve JavaScript'den başlayarak bileşeni adım adım oluşturacağız.

Bu bileşeni oluştururken yapılan aşamalı iyileştirmeler hakkında bilgi edinmek için Hikayeler bileşeni oluşturma başlıklı blog yayınımı inceleyin.

Kurulum

  1. Projeyi düzenlenebilir hale getirmek için Düzenlemek için remiks oluştur'u tıklayın.
  2. app/index.html adlı kişiyi aç.

HTML

Her zaman semantik HTML kullanmayı hedefliyorum. Her arkadaş istediği sayıda hikaye paylaşabileceğinden, her arkadaş için bir <section> öğesi ve her hikaye için bir <article> öğesi kullanmanın anlamlı olacağını düşündüm. Baştan başlayalım. Öncelikle, hikayeler bileşenimiz için bir kapsayıcıya ihtiyacımız var.

<body> öğenize bir <div> öğesi ekleyin:

<div class="stories">

</div>

Arkadaşları temsil etmek için bazı <section> öğeleri ekleyin:

<div class="stories">
  <section class="user"></section>
  <section class="user"></section>
  <section class="user"></section>
  <section class="user"></section>
</div>

Hikayeleri temsil etmek için bazı <article> öğeleri ekleyin:

<div class="stories">
  <section class="user">
    <article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>
    <article class="story" style="--bg: url(https://picsum.photos/480/841);"></article>
  </section>
  <section class="user">
    <article class="story" style="--bg: url(https://picsum.photos/481/840);"></article>
  </section>
  <section class="user">
    <article class="story" style="--bg: url(https://picsum.photos/481/841);"></article>
  </section>
  <section class="user">
    <article class="story" style="--bg: url(https://picsum.photos/482/840);"></article>
    <article class="story" style="--bg: url(https://picsum.photos/482/843);"></article>
    <article class="story" style="--bg: url(https://picsum.photos/482/844);"></article>
  </section>
</div>
  • Hikayeler için prototip oluşturmaya yardımcı olmak amacıyla bir resim hizmeti (picsum.com) kullanıyoruz.
  • Her <article> öğesindeki style özelliği, yer tutucu yükleme tekniğinin bir parçasıdır. Bu teknik hakkında daha fazla bilgiyi sonraki bölümde bulabilirsiniz.

CSS

İçeriklerimiz stil için hazır. Bu temel bilgileri, kullanıcıların etkileşimde bulunmak isteyeceği bir şeye dönüştürelim. Bugün mobil öncelikli çalışmaya başlayacağız.

.stories

<div class="stories"> kapsayıcımız için yatay kaydırmalı bir kapsayıcı istiyoruz. Bunu aşağıdaki yöntemlerle yapabiliriz:

  • Kapsayıcıyı ızgara yapma
  • Her çocuğu satır kanalını dolduracak şekilde ayarlama
  • Her alt öğenin genişliğini bir mobil cihazın görüntü alanının genişliğine ayarlama

HTML öğelerinizin tümü işaretlemenize yerleştirilene kadar ızgara, öncekinin sağ tarafına 100vw genişliğinde yeni sütunlar yerleştirmeye devam eder.

Chrome ve Geliştirici Araçları, tam genişlikte düzeni gösteren bir ızgara görseliyle açılır
Yatay kaydırma çubuğu oluşturan, ızgara sütununun taştığını gösteren Chrome Geliştirici Araçları.

app/css/index.css dosyasının alt kısmına aşağıdaki CSS'yi ekleyin:

.stories {
  display: grid;
  grid: 1fr / auto-flow 100%;
  gap: 1ch;
}

Görüntü alanının dışına çıkan bir içeriğimiz olduğuna göre, bu kapsayıcıya içeriği nasıl işleyeceğini söylemenin zamanı geldi. Vurgulanan kod satırlarını .stories kural kümenize ekleyin:

.stories {
  display: grid;
  grid: 1fr / auto-flow 100%;
  gap: 1ch;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  overscroll-behavior: contain;
  touch-action: pan-x;
}

Yatay kaydırma istiyoruz. Bu nedenle overflow-x değerini auto olarak ayarlıyoruz. Kullanıcı kaydırdığında bileşenin bir sonraki hikayeye yumuşak bir şekilde yerleşmesini istediğimizden scroll-snap-type: x mandatory değerini kullanırız. Bu CSS hakkında daha fazla bilgiyi blog yayınımın CSS Kaydırma Sabitleme Noktaları ve overscroll-behavior bölümlerinde bulabilirsiniz.

Kaydırma yapmayı kabul etmeleri için hem üst kapsayıcının hem de alt öğelerin kabul edilmesi gerekir, o yüzden bunu şimdi ele alalım. app/css/index.css dosyasının altına aşağıdaki kodu ekleyin:

.user {
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

Uygulamanız henüz çalışmıyor ancak scroll-snap-type etkinleştirildiğinde ve devre dışı bırakıldığında ne olacağı aşağıdaki videoda gösterilmektedir. Bu özellik etkinleştirildiğinde, her yatay kaydırma işlemi bir sonraki habere gider. Devre dışı bırakıldığında tarayıcı, varsayılan kaydırma davranışını kullanır.

Bu işlem, arkadaşlarınızın arasında gezinmenizi sağlar ancak hikayelerle ilgili çözmemiz gereken bir sorun var.

.user

.user bölümünde, bu alt hikaye öğelerini yerine yerleştirecek bir düzen oluşturalım. Bu sorunu çözmek için kullanışlı bir yığın oluşturma hilesi kullanacağız. Esasen, satır ve sütunun aynı [story] ızgaraya sahip olduğu 1x1'lik bir ızgara oluşturuyoruz. Her bir hikaye tablosu öğesi, söz konusu alanı talep etmeye çalışacak ve bu da bir yığın ortaya çıkacak.

Vurgulanan kodu .user kural kümenize ekleyin:

.user {
  scroll-snap-align: start;
  scroll-snap-stop: always;
  display: grid;
  grid: [story] 1fr / [story] 1fr;
}

app/css/index.css dosyasının en altına aşağıdaki kural kümesini ekleyin:

.story {
  grid-area: story;
}

Şimdi, mutlak konumlandırma, yüzer öğeler veya bir öğeyi akıştan çıkaran diğer düzen yönergeleri olmadan akışta olmaya devam ediyoruz. Üstelik, neredeyse hiç kod yok. Şuna bakın! Bu konu, videoda ve blog yayınında daha ayrıntılı olarak ele alınmıştır.

.story

Artık hikaye öğesinin stilini belirlememiz gerekiyor.

Daha önce, her <article> öğesindeki style özelliğinin yer tutucu yükleme tekniğinin bir parçası olduğundan bahsetmiştik:

<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>

Birden fazla arka plan resmi belirtmemize olanak tanıyan CSS'nin background-image özelliğini kullanacağız. Kullanıcı resmimizin en üstte olması ve yükleme tamamlandığında otomatik olarak görünmesi için bunları sıralayabiliriz. Bunu etkinleştirmek için resim URL'mizi özel bir mülke (--bg) yerleştirip yükleme yer tutucusuyla katman oluşturmak için CSS'mizde kullanırız.

Öncelikle, .story kural kümesini, yükleme işlemi tamamlandıktan sonra degradeyi arka plan resmiyle değiştirecek şekilde güncelleyelim. Vurgulanan kodu .story kural grubunuza ekleyin:

.story {
  grid-area: story;

  background-size: cover;
  background-image:
    var(--bg),
    linear-gradient(to top, lch(98 0 0), lch(90 0 0));
}

background-size değerini cover olarak ayarlamak, görüntümüzün tüm alanı dolduracağı için görüntüleme alanında boş alan kalmamasını sağlar. 2 arka plan resmi tanımlamak, yükleniyor mezar taşı adlı güzel bir CSS web hilesi yapmamızı sağlar:

  • Arka plan resmi 1 (var(--bg)), HTML'de satır içi olarak ilettiğimiz URL'dir.
  • 2. arka plan resmi (linear-gradient(to top, lch(98 0 0), lch(90 0 0)), URL yüklenirken gösterilecek bir degradedir.

Resim indirildikten sonra CSS, degradeyi otomatik olarak resimle değiştirir.

Ardından, bazı davranışları kaldırmak için CSS ekleyeceğiz. Böylece tarayıcı daha hızlı hareket edebilecek. Vurgulanan kodu .story kural kümenize ekleyin:

.story {
  grid-area: story;

  background-size: cover;
  background-image:
    var(--bg),
    linear-gradient(to top, lch(98 0 0), lch(90 0 0));

  user-select: none;
  touch-action: manipulation;
}
  • user-select: none, kullanıcıların yanlışlıkla metin seçmesini engeller
  • touch-action: manipulation, tarayıcıya bu etkileşimlerin dokunma etkinlikleri olarak değerlendirilmesi gerektiğini bildirir. Bu sayede tarayıcı, bir URL'yi tıklayıp tıklamadığınıza karar vermek zorunda kalmaz.

Son olarak, hikayeler arasındaki geçişi canlandırmak için küçük bir CSS ekleyelim. Vurgulanan kodu .story kural kümenize ekleyin:

.story {
  grid-area: story;

  background-size: cover;
  background-image:
    var(--bg),
    linear-gradient(to top, lch(98 0 0), lch(90 0 0));

  user-select: none;
  touch-action: manipulation;

  transition: opacity .3s cubic-bezier(0.4, 0.0, 1, 1);

  &.seen {
    opacity: 0;
    pointer-events: none;
  }
}

.seen sınıfı, çıkış gerektiren bir hikayeye eklenecek. Özel yumuşatma işlevini (cubic-bezier(0.4, 0.0, 1,1)) Materyal Tasarım'ın Yumuşak Geçiş kılavuzundan aldım (kaydırarak Hızlandırılmış yumuşatma bölümüne gidin).

Dikkatli bir gözünüz varsa muhtemelen pointer-events: none beyanını fark etmiş ve şu anda kafanızı kaşıyorsunuzdur. Bence çözümün şu ana kadar tek dezavantajı bu. .seen.story öğesi üstte olacağı ve görünmez olsa bile dokunma alacağı için buna ihtiyacımız vardır. pointer-events değerini none olarak ayarlayarak cam hikayesini pencereye dönüştürür ve kullanıcı etkileşimlerini artık çalmazız. Bu durum çok da kötü değil. Şu anda CSS'mizde bunu yönetmek çok zor değil. z-index ile ilgilenmiyoruz. Bu konuda hâlâ iyi hissediyorum.

JavaScript

Hikayeler bileşeninin etkileşimleri kullanıcı için oldukça basittir: İlerlemek için sağ tarafa, geri dönmek için sol tarafa dokunun. Kullanıcılar için basit olan şeyler, geliştiriciler için zor olabilir. Ancak çoğunu biz halledeceğiz.

Kurulum

Başlangıç olarak mümkün olduğunca fazla bilgi hesaplayıp depolayalım. Aşağıdaki kodu app/js/index.js dosyasına ekleyin:

const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

İlk JavaScript satırımız, birincil HTML öğe kökümüze referans alır ve bu referansı depolar. Sonraki satırda, öğemizin ortasının nerede olduğu hesaplanır. Böylece, bir dokunuşun ileri mi yoksa geri mi gideceğine karar verebiliriz.

Eyalet

Ardından, mantığımızla alakalı bazı durumlar içeren küçük bir nesne oluştururuz. Bu durumda, yalnızca mevcut hikayeyle ilgileniriz. HTML işaretlememizde, 1. arkadaşı ve en son hikayesini alarak bu bilgilere erişebiliriz. Vurgulanan kodu app/js/index.js'ünüze ekleyin:

const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

const state = {
  current_story: stories.firstElementChild.lastElementChild
}

Dinleyiciler

Artık kullanıcı etkinliklerini dinlemeye ve yönlendirmeye başlamak için yeterli mantıksal yapıya sahibiz.

fare

Hikayeler kapsayıcımızdaki 'click' etkinliğini dinleyerek başlayalım. Vurgulanan kodu app/js/index.js kampanyasına ekleyin:

const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

const state = {
  current_story: stories.firstElementChild.lastElementChild
}

stories.addEventListener('click', e => {
  if (e.target.nodeName !== 'ARTICLE')
    return

  navigateStories(
    e.clientX > median
      ? 'next'
      : 'prev')
})

Bir tıklama gerçekleşirse ve bu tıklama <article> öğesinde değilse işlemden vazgeçer ve hiçbir şey yapmayız. Makale ise clientX ile farenin veya parmağın yatay konumunu alırız. navigateStories henüz uygulanmadı ancak aldığı bağımsız değişken, hangi yönde ilerlememiz gerektiğini belirtir. Söz konusu kullanıcı konumu ortanca değerin üzerindeyse next'e, aksi takdirde prev'e (önceki) gitmemiz gerektiğini biliriz.

Klavye

Şimdi klavye tuşlarına basma işlemlerini dinleyelim. Aşağı ok tuşuna basılırsa next'ye gideriz. Yukarı Ok'u gösteriyorsa prev'ye gideceğiz.

Vurgulanan kodu app/js/index.js dosyasına ekleyin:

const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

const state = {
  current_story: stories.firstElementChild.lastElementChild
}

stories.addEventListener('click', e => {
  if (e.target.nodeName !== 'ARTICLE')
    return

  navigateStories(
    e.clientX > median
      ? 'next'
      : 'prev')
})

document.addEventListener('keydown', ({key}) => {
  if (key !== 'ArrowDown' || key !== 'ArrowUp')
    navigateStories(
      key === 'ArrowDown'
        ? 'next'
        : 'prev')
})

Hikayeler'de gezinme

Artık hikayelerin benzersiz iş mantığını ve bu içeriklerin sağladığı kullanıcı deneyimini ele almanın zamanı geldi. Bu kod blok halinde ve karmaşık görünüyor ancak satır satır incelerseniz oldukça anlaşılır olduğunu göreceksiniz.

Öncelikle, bir arkadaşa gidip gitmeyeceğimiz veya bir hikayeyi gösterip göstermeyeceğimiz konusunda karar vermemize yardımcı olan bazı seçicileri saklıyoruz. HTML'de çalıştığımız için HTML'yi arkadaş (kullanıcı) veya hikaye (hikaye) varlığı için sorgulayacağız.

Bu değişkenler, "x hikayesi için "sonraki", aynı arkadaştan başka bir hikayeye mi yoksa başka bir arkadaşa mı geçmek anlamına geliyor?" Kendi geliştirdiğimiz ağaç yapısını kullanarak ebeveynlere ve çocuklarına ulaşarak yaptım.

app/js/index.js dosyasının altına aşağıdaki kodu ekleyin:

const navigateStories = direction => {
  const story = state.current_story
  const lastItemInUserStory = story.parentNode.firstElementChild
  const firstItemInUserStory = story.parentNode.lastElementChild
  const hasNextUserStory = story.parentElement.nextElementSibling
  const hasPrevUserStory = story.parentElement.previousElementSibling
}

İş mantığı hedefimiz, mümkün olduğunca doğal dile yakın şekilde şöyledir:

  • Dokunmanın nasıl gerçekleştirileceğine karar verme
    • Sonraki/önceki bir hikaye varsa: İlgili hikayeyi göster
    • Bu, arkadaşınızın son/ilk hikayesiyse: Yeni bir arkadaşınıza gösterin
    • Bu yönde gidecek bir hikaye yoksa: hiçbir şey yapmayın
  • Yeni mevcut hikayeyi state klasörüne sakla

Vurgulanan kodu navigateStories işlevinize ekleyin:

const navigateStories = direction => {
  const story = state.current_story
  const lastItemInUserStory = story.parentNode.firstElementChild
  const firstItemInUserStory = story.parentNode.lastElementChild
  const hasNextUserStory = story.parentElement.nextElementSibling
  const hasPrevUserStory = story.parentElement.previousElementSibling

  if (direction === 'next') {
    if (lastItemInUserStory === story && !hasNextUserStory)
      return
    else if (lastItemInUserStory === story && hasNextUserStory) {
      state.current_story = story.parentElement.nextElementSibling.lastElementChild
      story.parentElement.nextElementSibling.scrollIntoView({
        behavior: 'smooth'
      })
    }
    else {
      story.classList.add('seen')
      state.current_story = story.previousElementSibling
    }
  }
  else if(direction === 'prev') {
    if (firstItemInUserStory === story && !hasPrevUserStory)
      return
    else if (firstItemInUserStory === story && hasPrevUserStory) {
      state.current_story = story.parentElement.previousElementSibling.firstElementChild
      story.parentElement.previousElementSibling.scrollIntoView({
        behavior: 'smooth'
      })
    }
    else {
      story.nextElementSibling.classList.remove('seen')
      state.current_story = story.nextElementSibling
    }
  }
}

Deneyin

  • Siteyi önizlemek için Uygulamayı Görüntüle'ye basın. Ardından, Tam ekran tam ekran düğmesine basın.

Sonuç

Bu e-posta, bileşenle ilgili ihtiyaçlarımı özetlemiş oldu. Bunun üzerine inşa etmekten, verilerden yola çıkarak artırmaktan ve genel olarak kendinize özel hale getirmekten çekinmeyin.