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
- Projeyi düzenlenebilir hale getirmek için Düzenlenecek remiks'i tıklayın.
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>
öğesindekistyle
ö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.
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 ö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ı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 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!