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 olarak çalışacağı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ğiyle aynı hale getirme

HTML öğelerinin 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 DevTools, tam genişlik düzenini 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 öğesinin 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ırmayla sabitlemeyi hem üst kapsayıcı hem de alt kapsayıcıların kabul etmesi gerekir. Bu nedenle, şimdi bu konuyu 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 aşağıdaki videoda, scroll-snap-type etkinleştirildiğinde ve devre dışı bırakıldığında ne olduğu 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] ızgara takma adına sahip olduğu 1x1 boyutunda bir ızgara oluşturuyoruz. Her hikaye ızgara öğesi bu alanı kullanmaya çalışacak ve sonuçta bir yığın oluşturulur.

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. Ayrıca, neredeyse hiç kod yok. 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>

CSS'nin background-image mülkünü kullanacağız. Bu mülk, birden fazla arka plan resmi belirtmemize olanak tanır. Kullanıcı resmimizin en üstte olması ve yükleme işlemi tamamlandığında otomatik olarak görünmesi için bunları bir sıraya koyabiliriz. 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 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));
}

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 animasyonlu hale getirmek için biraz 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 eklenir. Özel yumuşatma işlevini (cubic-bezier(0.4, 0.0, 1,1)), Materyal Tasarım'ın Yumuşatma kılavuzundan aldım (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. Bu çözümün şimdiye kadarki tek dezavantajı bu diyebilirim. .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: İleri gitmek için sağa, geri gitmek için sola 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'inize 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 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')
})

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 ise prev'ye gideriz.

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 gizlemeyeceğimize 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 hikayesinde "sonraki" ifadesi aynı arkadaştan başka bir hikayeye mi yoksa farklı bir arkadaşa mı geçmeyi mi ifade eder?" gibi soruları yanıtlamamıza yardımcı olur. Bunu, oluşturduğumuz ağaç yapısını kullanarak ebeveynlere ve çocuklarına ulaştık.

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:

  • Musluğu nasıl işleyeceğinize karar verin
    • Sonraki/önceki bir hikaye varsa: İlgili hikayeyi göster
    • Arkadaşın son/ilk hikayesiyse: Yeni bir arkadaş göster
    • Bu yönde gidecek bir hikaye yoksa: hiçbir şey yapmayın
  • Yeni mevcut hikayeyi state'e ekleme

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, ardından Tam Ekran'a tam ekran basın.

Sonuç

Bileşenle ilgili ihtiyaçlarımızı özetledik. Bu şablonu temel alarak geliştirme yapabilir, verilerle destekleyebilir ve genel olarak kendi şablonunuz haline getirebilirsiniz.