Bu codelab'de, web'de Instagram Hikayeleri'ne benzer bir deneyim oluşturmayı öğreneceksiniz. Bileşeni adım adım oluşturacağız. Önce HTML, sonra CSS, ardından JavaScript ile çalışacağız.
Bu bileşeni oluştururken yapılan progresif geliştirmeler hakkında bilgi edinmek için Hikayeler bileşeni oluşturma başlıklı blog yayınımı inceleyin.
Kurulum
- Projeyi düzenlenebilir hale getirmek için Düzenlemek için remiksle'yi tıklayın.
app/index.htmladlı kişiyi aç.
HTML
Her zaman semantik HTML kullanmaya çalışırım.
Her arkadaşın istediği sayıda 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.
Ancak en 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 eden 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 edecek 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>öğesindekistyleö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
İçeriğimiz stil için hazır. Şimdi bu iskeleti, kullanıcıların etkileşime geçmek isteyeceği bir şeye dönüştürelim. Bugün mobil öncelikli çalışacağız.
.stories
<div class="stories"> kapsayıcımız için yatay kaydırmalı bir kapsayıcı istiyoruz.
Bunu şu şekilde yapabiliriz:
- Kapsayıcıyı Izgara yapma
- Her alt öğeyi satır kanalını dolduracak şekilde ayarlama
- Her alt öğenin genişliğini mobil cihaz görüntü alanının genişliğiyle aynı yapma
Izgara, işaretlemenizdeki tüm HTML öğelerini yerleştirene kadar yeni 100vw genişliğindeki sütunları bir öncekinin sağına yerleştirmeye devam eder.
app/css/index.css dosyasının en altı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 taşan içeriğimiz olduğuna göre artık kapsayıcıya bu içeriği nasıl işleyeceğini söyleyebiliriz. 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ı kaydırdığında bileşenin bir sonraki hikayeye yumuşak bir şekilde geçmesini istediğimiz için scroll-snap-type: x mandatory kullanacağız. Bu CSS hakkında daha fazla bilgiyi blog yayınımın CSS Kaydırarak Tutturma Noktaları ve overscroll-behavior bölümlerinde bulabilirsiniz.
Kaydırarak tutturma özelliğinin kullanılabilmesi için hem üst kapsayıcının hem de alt öğelerin bu özelliği kabul etmesi gerekir. Şimdi bu işlemi yapalı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 işlemi bir sonraki hikayeye kaydırarak tutturulur. Devre dışı bırakıldığında tarayıcı, varsayılan kaydırma davranışını kullanır.
Bu sayede arkadaşlarınız arasında gezinebilirsiniz ancak hikayelerle ilgili çözmemiz gereken bir sorun daha var.
.user
.user bölümünde, alt hikaye öğelerini yerlerine yerleştiren bir düzen oluşturalım. Bu sorunu çözmek için kullanışlı bir yığınlama hilesi kullanacağız.
Aslında satır ve sütunun aynı [story] ızgara takma adına sahip olduğu 1x1'lik bir ızgara oluşturuyoruz. Her hikaye ızgara öğesi bu alanı talep etmeye çalışacak ve sonuçta bir yığın oluşacak.
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 grubunu app/css/index.css dosyasının en altına ekleyin:
.story {
grid-area: story;
}
Şimdi, mutlak konumlandırma, kayan öğeler veya bir öğeyi akışın dışına çıkaran diğer düzen yönergeleri olmadan hâlâ akıştayız. Üstelik neredeyse hiç kod yok, şuna bakın! Bu konu, videoda ve blog yayınında daha ayrıntılı olarak ele alınmaktadır.
.story
Şimdi de hikaye öğesinin kendisini biçimlendirmemiz gerekiyor.
Daha önce, her <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>
CSS'nin birden fazla arka plan resmi belirtmemize olanak tanıyan background-image özelliğini kullanacağız. Bunları, 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ştirir ve CSS'mizde yükleme yer tutucusuyla katman oluşturmak için kullanırız.
Öncelikle, yükleme işlemi tamamlandığında gradyanı bir arka plan resmiyle değiştirmek için .story kural kümesini 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 özelliğini cover olarak ayarlamak, resmimiz dolduracağı için görüntü alanında boş alan kalmamasını sağlar. İki arka plan resmi tanımlamak, yükleme yer tutucusu adı verilen şık bir CSS web hilesi kullanmamıza olanak tanır:
- 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 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 önlertouch-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ışmak zorunda kalmaz.
Son olarak, hikayeler arasındaki geçişe animasyon eklemek için biraz 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ış gerektiren bir hikayeye eklenecek.
Ö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 pointer-events: none bildirimini fark etmiş ve şu anda kafanızı kaşıyor olabilirsiniz. Çözümün şu ana kadarki tek
olumsuz yanı bu diyebilirim. .seen.story öğesi görünmez olsa bile en üstte yer alacağı ve dokunmaları alacağı için bu gereklidir. pointer-events değerini none olarak ayarlayarak cam hikayeyi bir pencereye dönüştürür ve kullanıcı etkileşimlerini çalmayız. Bu, fena bir ödün değil. CSS'mizde bunu yönetmek şu anda çok zor değil. z-index ile hokkabazlık yapmıyoruz. Bu konuda hâlâ iyimserim.
JavaScript
Hikayeler bileşeninin etkileşimleri kullanıcı için oldukça basittir: İleri gitmek için sağa, geri gitmek için sola dokunulur. Kullanıcılar için basit olan şeyler, geliştiriciler için zor olabilir. Ancak bu işlerin çoğunu biz hallederiz.
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)
JavaScript'imizin ilk satırı, birincil HTML öğesi kökümüze bir referans alır ve depolar. Sonraki satır, öğemizin ortasının nerede olduğunu hesaplar. Böylece dokunmanın ileri mi yoksa geri mi gideceğine karar verebiliriz.
Eyalet
Ardından, mantığımızla ilgili bir duruma sahip küçük bir nesne oluştururuz. Bu durumda, yalnızca mevcut hikayeyle ilgileniyoruz. HTML işaretlememizde, ilk arkadaşı ve en son hikayesini alarak buna erişebiliriz. Vurgulanan kodu app/js/index.js
öğenize 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
Hikaye 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')
})
Tıklama gerçekleşirse ve <article> öğesinde değilse işlemi iptal eder ve hiçbir şey yapmayız.
Makale ise farenin veya parmağın yatay konumunu clientX ile alırız. navigateStories işlevini henüz uygulamadık ancak aldığı bağımsız değişken, gitmemiz gereken yönü belirtir. Kullanıcının konumu ortanca değerden büyükse next, aksi takdirde prev (önceki) yönüne gitmemiz gerektiğini biliriz.
Klavye
Şimdi klavye tuşlarına basılmasını dinleyelim. Aşağı ok tuşuna basılırsa next sayfasına yönlendiriliriz. 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
Şimdi, hikayelerin benzersiz iş mantığını ve ünlü oldukları kullanıcı deneyimini ele alma zamanı. Bu biraz karmaşık ve zor görünüyor ancak satır satır incelerseniz oldukça anlaşılır olduğunu göreceksiniz.
Öncelikle, bir arkadaşa kaydırma yapıp yapmayacağı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 (kullanıcılar) veya hikayelerin (hikaye) varlığını sorgulayacağız.
Bu değişkenler, "X hikayesi için 'sonraki' seçeneği, aynı arkadaşın başka bir hikayesine mi yoksa farklı bir arkadaşın hikayesine mi geçiş anlamına gelir?" gibi soruları yanıtlamamıza yardımcı olur. Bunu, oluşturduğumuz ağaç yapısını kullanarak, üst ve alt öğelere 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
}
Doğal dile mümkün olduğunca yakın olan iş mantığı hedefimiz şöyledir:
- Dokunma işleminin nasıl ele alınacağına karar verme
- Sonraki/önceki hikaye varsa: O hikayeyi göster
- Arkadaşın son/ilk hikayesiyse: Yeni bir arkadaş göster
- O yönde gidilecek hikaye yoksa: Hiçbir şey yapma
- Yeni mevcut hikayeyi
stateiçine 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'a
basın.
Sonuç
Bileşenle ilgili ihtiyaçlarım bu kadar. Bu şablonu dilediğiniz gibi geliştirebilir, verilerle destekleyebilir ve kendinize göre uyarlayabilirsiniz.