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
- Projeyi düzenlenebilir hale getirmek için Düzenlemek için remiks oluştur'u tıklayın.
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>
öğ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
İç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.
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 engellertouch-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 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.