Codelab: Hikayeler bileşeni oluşturma

Bu codelab'de, web'de Instagram Stories gibi bir deneyimi nasıl oluşturacağınızı öğrenebilirsiniz. Bileşeni, ilerledikçe oluşturacağız. Önce HTML, ardından CSS, ardından JavaScript'i kullanacağız.

Bu bileşeni oluştururken yapılan aşamalı geliştirmeler hakkında bilgi edinmek için Hikayeler bileşeni oluşturma adlı blog yayınıma göz atın.

Kurulum

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

HTML

Ben her zaman semantik HTML kullanmayı hedefliyorum. Her arkadaşın birden fazla hikayesi olabileceğinden, her arkadaş için bir <section> öğesi ve her hikaye için bir <article> öğesi kullanmanın anlamlı olacağını düşündüm. Yine de en baştan başlayalım. İlk olarak, hikayeler bileşenimiz için bir kapsayıcıya ihtiyacımız var.

<body> için bir <div> öğesi ekleyin:

<div class="stories">

</div>

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

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

Haberleri temsil edecek birkaç <article> öğesi 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>
  • Hikayelerin prototiplenmesine yardımcı olması için bir resim hizmeti (picsum.com) kullanıyoruz.
  • Her bir <article> öğesindeki style özelliği, bir sonraki bölümde hakkında daha fazla bilgi edineceğiniz yer tutucu yükleme tekniğinin bir parçasıdır.

CSS

İçeriklerimiz stile hazır. Gelin bu kemikleri, insanların etkileşim kurmak istediği bir şeye dönüştürelim. Bugün mobil öncelikli çalışmaya başlıyoruz.

.stories

<div class="stories"> kapsayıcısı için yatay kaydırmalı bir kapsayıcı istiyoruz. Bunu başarmak için şunları yapabiliriz:

  • Container'ı Izgara yapma
  • Her alt öğeyi satır kanalını dolduracak şekilde ayarlama
  • Her alt öğenin genişliğini bir mobil cihaz görüntü alanının genişliği kadar yapmak

Izgara, işaretlemenize tüm HTML öğeleri yerleştirilinceye kadar 100vw genişliğinde yeni sütunları bir öncekinin sağına yerleştirmeye devam eder.

Chrome ve Geliştirici Araçları, tam genişlikte düzeni gösteren bir ızgara görseliyle açılır
Chrome Geliştirici Araçları, ızgara sütunu taşmalarını göstererek yatay kaydırıcıyı gösteriyor.

app/css/index.css öğesinin altına aşağıdaki CSS'yi ekleyin:

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

Artık görüntü alanının ötesine geçen bir içeriğimiz olduğuna göre, kapsayıcıya bunun nasıl ele alınacağını söylemenin zamanı geldi. Vurgulanan kod satırlarını .stories kural grubunuza 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 istediğimiz için overflow-x değerini auto olarak ayarlayacağız. Kullanıcı ekranı kaydırdığında bileşenin bir sonraki hikayeye yavaşça dönmesini isteriz. Bu yüzden, scroll-snap-type: x mandatory kullanacağız. Blog yayınımın CSS Kaydırma Noktaları ve fazla kaydırma davranışı bölümlerinden bu CSS hakkında daha fazla bilgi edinebilirsiniz.

Kaydırmayı tutturmayı kabul etmek için hem üst kapsayıcıyı hem de alt öğeleri kabul ederiz. Bu yüzden şimdi bu konuyu halledelim. app/css/index.css kodunun altına şu kodu ekleyin:

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

Uygulamanız henüz çalışmıyor. Ancak scroll-snap-type etkinleştirilip devre dışı bırakıldığında neler olduğu aşağıdaki videoda gösterilmektedir. Etkinleştirildiğinde, her yatay kaydırma bir sonraki hikayeye tutturur. Devre dışı bırakıldığında tarayıcı varsayılan kaydırma davranışını kullanır.

Böylece arkadaşlarınız arasında gezinebilirsiniz, ama hâlâ çözmemiz gereken hikayelerle ilgili bir sorunumuz var.

.user

.user bölümünde, bu alt hikaye öğelerini yerleştiren bir düzen oluşturalım. Bu sorunu çözmek için kullanışlı bir yığın numarası kullanacağız. Esas olarak, satır ve sütunun aynı Izgara takma adına ([story]) sahip olduğu 1x1 bir ızgara oluşturuyoruz ve her hikaye ızgara öğesi bu alanı talep etmeye çalışacak ve sonuçta bir yığın elde edilecektir.

Vurgulanan kodu .user kural grubunuza ekleyin:

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

Aşağıdaki kural kümesini app/css/index.css öğesinin altına ekleyin:

.story {
  grid-area: story;
}

Şimdi, bir öğeyi akıştan çıkaran mutlak konumlandırma, float veya diğer düzen yönergeleri olmadan hâlâ akış içindeyiz. Ayrıca, neredeyse hiç kod yok. Şuna bakın! Bu konu videoda ve blog yayınında daha ayrıntılı bir şekilde açıklanıyor.

.story

Şimdi sadece hikaye öğesinin stilini belirlememiz gerekiyor.

Daha önce, her bir <article> öğesindeki style özelliğinin bir yer tutucu yükleme tekniğinin 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. Bunları, kullanıcı resmimizin en üstte yer alacağı ve yükleme tamamlandığında otomatik olarak görünecek şekilde bir sıraya koyabiliriz. Bunu etkinleştirmek için resim URL'mizi özel bir özelliğe (--bg) yerleştirecek ve yükleme yer tutucusunu katman olarak eklemek için CSS'mizde kullanacağız.

İlk olarak, .story kural kümesini, yükleme işlemi bittiğinde bir renk geçişini 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 öğesinin cover değerine ayarlanması, görüntümüzdeki alanı dolduracağı için görüntü alanında boş alan olmamasını sağlar. 2 arka plan resmi tanımlamak, yükleme tombstone adı verilen düzenli bir CSS web numarası elde etmemizi sağlar:

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

Resmin indirilmesi tamamlandığında CSS otomatik olarak gradyanı resimle değiştirir.

Ardından, bazı davranışları ortadan kaldırmak için CSS ekleyerek tarayıcının daha hızlı çalışmasını sağlayacağız. 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));

  user-select: none;
  touch-action: manipulation;
}
  • user-select: none, kullanıcıların yanlışlıkla metin seçmesini önler
  • touch-action: manipulation, tarayıcıya bu etkileşimlerin dokunma etkinlikleri olarak değerlendirilmesi gerektiğini bildirir. Böylece tarayıcı, bir URL'yi tıklayıp tıklamadığınız konusunda karar vermeye çalışmak zorunda kalmaz

Son olarak, hikayeler arasındaki geçişi canlandırmak için küçük bir CSS ekleyelim. 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));

  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ış yapılması gereken bir hikayeye eklenecek. Özel yumuşak geçiş işlevini (cubic-bezier(0.4, 0.0, 1,1)) Materyal Tasarım'ın Yumuşak Geçiş kılavuzundan (kaydırarak Hızlandırılmış yumuşak geçiş bölümüne gidin) aldım.

Dikkatli bir gözünüz varsa muhtemelen pointer-events: none beyanını fark etmişsinizdir ve şu anda kafanızı şaştırmışsınızdır. Bu sanırım çözümün şimdiye kadar tek dezavantajı bu. Bir .seen.story öğesi üstte yer alacağı ve görünmez olsa bile dokunmaları alacağı için buna ihtiyacımız var. pointer-events özelliğini none olarak ayarlayarak cam hikayeyi bir pencereye dönüştürür ve başka kullanıcı etkileşimlerini çalmayız. Ödün vermemiz ne kadar da zor değil. z-index ile hokkabazlık yapmıyoruz. Bu konuda hâlâ iyi hissediyorum.

JavaScript

Hikayeler bileşeninin etkileşimleri kullanıcı için oldukça basittir: İlerlemek için sağa, geri dönmek için sola dokunun. Kullanıcıların basit işleri, geliştiriciler için genellikle zor olur. Yine de çoğunu biz halledeceğiz.

Kurulum

İlk olarak, yapabildiğimiz kadar fazla bilgiyi hesaplayıp depolayalım. Şu kodu app/js/index.js hesabına ekleyin:

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

İlk JavaScript satırımız, birincil HTML öğesi kökümüze bir başvuru yakalar ve saklar. Bir sonraki satır, öğemizin ortasının nerede olduğunu hesaplar. Böylece bir dokunmanın ileri mi yoksa geri mi olacağına karar verebiliriz.

Eyalet

Daha sonra, mantığımıza uygun bir durum içeren küçük bir nesne oluştururuz. Bu örnekte yalnızca mevcut hikayeyle ilgileniyoruz. HTML işaretlememizde bu bilgiye, birinci arkadaşı ve onun en son hikayesini yakalayarak erişebiliriz. Vurgulanan kodu app/js/index.js cihazınıza 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ığımız var.

fare

Hikayeler kapsayıcımızda 'click' etkinliğini dinleyerek başlayalım. Vurgulanan kodu app/js/index.js içine 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')
})

Tıklama gerçekleşirse ve bu <article> öğesinde değilse kefalet ederiz ve hiçbir şey yapmayız. Öğe bir makaleyse farenin veya parmağın yatay konumunu clientX ile yakalarız. navigateStories uygulamasını henüz uygulamadık ancak bunun verdiği bağımsız değişken hangi yöne gitmemiz gerektiğini belirtiyor. Söz konusu kullanıcı konumu ortanca değerden büyükse next konumuna gitmemiz gerektiğini, aksi takdirde prev (önceki) değerine gitmemiz gerektiğini biliriz.

Klavye

Şimdi klavyeye basmaları dinleyelim. Aşağı Ok tuşuna basarsanız next konumuna gideriz. Yukarı Ok ise prev bölümüne gideriz.

Vurgulanan kodu app/js/index.js içine 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

Hikayelerin benzersiz iş mantığını ve ün kazanan kullanıcı deneyimini ele alma zamanı. Bu karışık ve zor görünüyor, ama satır satır anlatırsanız oldukça kolay anlaşılır olduğunu göreceksiniz.

Öncelikli olarak bir arkadaşa gitmeye mi yoksa bir hikayeyi göstermeye/gizlenmeye mi karar vermemize yardımcı olan bazı seçicileri saklarız. Çalıştığımız yer HTML olduğundan, arkadaşların (kullanıcılar) veya hikayelerin (hikaye) varlığını sorgulayacağız.

Bu değişkenler, "x hikayesi verilen, "sonraki" bir başka habere aynı arkadaştan mı yoksa başka bir arkadaşa mı geçmek anlamına geliyor? bunu kendi oluşturduğumuz ağaç yapısını kullanarak, ebeveynlere ve çocuklarına ulaşarak yaptım.

app/js/index.js kodunun altına şu 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
}

En doğal dile yakın bir şekilde iş mantığı hedefimiz şöyledir:

  • Dokunuşun nasıl ele alınacağına karar verin
    • Önceki/sonraki bir haber varsa: O hikayeyi gösterin
    • Bu, arkadaşınızın son/ilk hikayesiyse: Yeni bir arkadaşınıza gösterin
    • Bu yönde ilerleyecek bir hikaye yoksa hiçbir şey yapmayın:
  • Mevcut yeni hikayeyi state uygulamasında 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öster'e, ardından Tam Ekran'a tam ekran basın.

Sonuç

Bileşenle ilgili ihtiyaçlarımla ilgili özet bilgi vermek istedik. Üzerinden geliştirmekten, verilerle desteklemekten ve genel olarak kendinize özel hale getirmekten çekinmeyin!