Codelab: Hikayeler bileşeni oluşturma

Bu codelab'de, web'de Instagram Hikayeleri gibi bir deneyim oluşturmayı öğreneceksiniz. Bileşeni, HTML ile başlayıp CSS ve JavaScript ile devam ederek 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 Remix'i tıklayın.
  2. app/index.html adlı kişiyi aç.

HTML

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

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

<div class="stories">

</div>

Arkadaşları temsil etmek için <section> öğeler 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>
  • Hikayelerin prototipini oluşturmaya yardımcı olması için 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 edinebilirsiniz.

CSS

İçeriğimiz stil için hazır. Bu iskeletleri, 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ırma kapsayıcısı istiyoruz. Bunu başarmak için:

  • Kapsayıcıyı ızgara yapma
  • Her alt öğeyi satır parçasını dolduracak şekilde ayarlama
  • Her alt öğenin genişliğini mobil cihazın görünüm alanı genişliği yapma

Izgara, işaretlemenizdeki tüm HTML öğelerini yerleştirene kadar önceki sütunun sağ tarafına yeni 100vw genişliğinde sütunlar yerleştirmeye devam eder.

Chrome ve Geliştirici Araçları, tam genişlik düzenini gösteren bir ızgara görseliyle açılıyor.
Chrome Geliştirici Araçları, yatay kaydırma çubuğu oluşturan ızgara sütunu taşmasını gösteriyor.

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

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

Artık görüntü alanının dışına taşan içeriğimiz olduğuna göre, kapsayıcıya bu içeriği nasıl işleyeceğini söyleme zamanı geldi. Kodun vurgulanan 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 istediğimiz için overflow-x değerini auto olarak ayarlayacağız. Kullanıcı kaydırdığında bileşenin bir sonraki hikayeye sorunsuz bir şekilde geçmesini istiyoruz. Bu nedenle scroll-snap-type: x mandatory kullanacağız. Bu CSS hakkında daha fazla bilgiyi blog yayınımın CSS Scroll Snap Points ve overscroll-behavior bölümlerinde bulabilirsiniz.

Kaydırma yapışma özelliğinin kullanılabilmesi için hem üst kapsayıcının hem de alt öğelerin kabul etmesi gerekir. Bu nedenle, bu durumu şimdi ele alalım. Aşağıdaki kodu app/css/index.css dosyasının en altına 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österiliyor. Etkinleştirildiğinde, her yatay kaydırma bir sonraki hikayeye geçer. Devre dışı bırakıldığında tarayıcı, varsayılan kaydırma davranışını kullanır.

Bu işlemle arkadaşlarınız arasında gezinebilirsiniz ancak çözmemiz gereken bir hikayeler sorunu var.

.user

.user bölümünde, alt öğe hikaye öğelerini yerlerine yerleştiren bir düzen oluşturalım. Bu sorunu çözmek için kullanışlı bir üst üste yerleştirme hilesi kullanacağız. Temel olarak, satır ve sütunun aynı [story] Grid takma adını kullandığı 1x1'lik bir ızgara oluşturuyoruz. Her hikaye ızgarası öğesi bu alanı talep etmeye çalışarak bir yığın oluşturuyor.

Vurgulanan kodu .user kural kümenize ekleyin:

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

Aşağıdaki kural grubunu app/css/index.css dosyasının en altına ekleyin:

.story {
  grid-area: story;
}

Artık mutlak konumlandırma, kaydırma veya bir öğeyi akıştan çıkaran diğer düzen yönergeleri olmadan akıştayız. Ayrıca, neredeyse hiç kod yok. Bakın! Bu konu, videoda ve blog yayınında daha ayrıntılı olarak ele alınmaktadır.

.story

Şimdi de hikaye öğesini stilize etmemiz gerekiyor.

Daha önce, her <article> öğesindeki style özelliğinin yer tutucu yükleme tekniğinin bir parçası olduğunu belirtmiş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ı resmimiz en üstte olacak ve yükleme tamamlandığında otomatik olarak görünecek şekilde sıralayabiliriz. Bunu etkinleştirmek için resim URL'mizi özel bir özelliğe (--bg) yerleştirip yükleme yer tutucusuyla katman oluşturmak üzere CSS'mizde kullanacağız.

Öncelikle, yükleme tamamlandıktan sonra gradyanı arka plan resmiyle değiştirmek için .story kural kümesini 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 ayarını cover olarak belirlemek, resmimiz bu alanı dolduracağı için görünüm penceresinde boş alan olmamasını sağlar. 2 arka plan resmi tanımlamak, yükleme mezar taşı adı verilen şık 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.
  • Arka plan resmi 2 (linear-gradient(to top, lch(98 0 0), lch(90 0 0)), URL yüklenirken gösterilecek bir gradyandır)

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

Ardından, bazı davranışları kaldırmak için CSS ekleyerek tarayıcının daha hızlı hareket etmesini sağlayacağız. 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 ö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ıza karar vermeye çalışmaktan kurtulur.

Son olarak, hikayeler arasındaki geçişi canlandırmak 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ış yapılması gereken bir hikayeye eklenir. Özel yumuşatma işlevini (cubic-bezier(0.4, 0.0, 1,1)), Materyal Tasarım'ın Yumuşatma kılavuzundan (Hızlandırılmış yumuşatma bölümüne gidin) aldım.

Dikkatliyseniz pointer-events: nonebeyanı fark etmiş ve şu anda kafanız karışmış olabilir. Şu ana kadar çözümün tek dezavantajı bu gibi görünüyor. Görünmez olsa bile .seen.story öğesi üstte olacağı ve dokunmaları alacağı için bu bilgiye ihtiyacımız var. pointer-events öğesini none olarak ayarlayarak cam hikayeyi pencereye dönüştürüyor ve kullanıcı etkileşimlerini çalmıyoruz. Bu, çok kötü bir değişim değil ve CSS'mizde şu anda yönetilmesi çok zor bir durum değil. z-index ile uğraşmıyoruz. 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 zorlu bir iş olabilir. Ancak bu işlemlerin büyük bir kısmını biz hallederiz.

Kurulum

Başlamak için mümkün olduğunca fazla bilgiyi 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)

JavaScript'imizin ilk satırı, birincil HTML öğesi kökümüzün referansını alıp depolar. Bir sonraki satır, öğemizin ortasının nerede olduğunu hesaplar. Böylece, dokunmanın ileri mi yoksa geri mi gitmek için yapıldığına 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 ilgileniyoruz. HTML işaretlememizde, 1. arkadaşı ve en son hikayesini alarak buna erişebiliriz. Vurgulanan kodu app/js/index.js adresinize 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ığa 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şir ve bu tıklama <article> öğesinde olmazsa hiçbir şey yapmayız. Makale ise fare veya parmağın yatay konumunu clientX ile alırız. navigateStories henüz uygulanmamıştır ancak bağımsız değişken, hangi yöne gitmemiz gerektiğini belirtir. Kullanıcı konumu ortalamadan büyükse next'ye, aksi takdirde prev'ye (önceki) gitmemiz gerekir.

Klavye

Şimdi klavye tuşlarına basıldığında sesi dinleyelim. Aşağı ok tuşuna basıldığında next konumuna gidilir. 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')
})

Hikayelerde gezinme

Hikayelerin benzersiz iş mantığı ve ünlü oldukları kullanıcı deneyimiyle ilgili sorunları ele alma zamanı. Bu işlem biraz karışık gibi görünse de satır satır incelediğinizde aslında oldukça kolay olduğunu göreceksiniz.

Başlangıçta, bir arkadaşa kaydırıp kaydırmayacağımıza veya bir hikayeyi gösterip göstermeyeceğimize karar vermemize yardımcı olan bazı seçicileri saklarız. HTML üzerinde çalıştığımız için arkadaşların (kullanıcılar) veya hikayelerin (hikaye) varlığını sorgulayacağız.

Bu değişkenler, "x hikayesi verildiğinde "sonraki" seçeneği, aynı arkadaşın başka bir hikayesine mi yoksa farklı bir arkadaşın hikayesine mi geçileceğini ifade ediyor?" gibi soruları yanıtlamamıza yardımcı olur. Bunu, oluşturduğumuz ağaç yapısını kullanarak, ebeveynlere ve çocuklarına ulaşarak yaptım.

Aşağıdaki kodu app/js/index.js dosyasının en altına 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ığı hedefimizi mümkün olduğunca doğal dile yakın bir şekilde aşağıda paylaşıyoruz:

  • Dokunma işleminin nasıl ele alınacağına karar verin.
    • Sonraki/önceki bir hikaye varsa o hikayeyi göster
    • Arkadaşın son/ilk hikayesiyse: Yeni bir arkadaş göster
    • O yönde gidilecek bir hikaye yoksa: hiçbir şey yapmayın
  • Yeni mevcut hikayeyi state klasörüne kaydedin.

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'a basın. tam ekran

Sonuç

Bileşenle ilgili ihtiyaçlarımın özeti bu şekilde. Bu şablonu geliştirebilir, verilerle destekleyebilir ve kendi ihtiyaçlarınıza göre uyarlayabilirsiniz.