feat(home): Featured 모바일 터치 스크롤·화살표 끝 비활성
- 트랙에 touch-pan-x·webkit 가로 스크롤·overscroll-x-contain 적용 - scroll·ResizeObserver로 이전/다음 disabled 동기화 - v0.0.60 문서 반영 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -88,6 +88,67 @@ const featuredPosts = computed(() => posts.value.slice(0, 6))
|
||||
const latestPosts = computed(() => posts.value.map(mapLatestPost))
|
||||
|
||||
const featuredTrackRef = ref(null)
|
||||
/** Featured 트랙이 스크롤 시작에 붙었는지 — 이전 화살표 비활성 */
|
||||
const featuredAtStart = ref(true)
|
||||
/** Featured 트랙이 스크롤 끝에 붙었는지 — 다음 화살표 비활성 */
|
||||
const featuredAtEnd = ref(true)
|
||||
|
||||
let unbindFeaturedScroll = () => {}
|
||||
|
||||
/**
|
||||
* Featured 가로 스크롤 위치에 따라 이전/다음 버튼 상태를 갱신한다.
|
||||
* @param {HTMLElement | null} el - 스크롤 컨테이너
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateFeaturedScrollEdges = (el) => {
|
||||
const target = el || featuredTrackRef.value
|
||||
|
||||
if (!target) {
|
||||
featuredAtStart.value = true
|
||||
featuredAtEnd.value = true
|
||||
return
|
||||
}
|
||||
|
||||
const { scrollLeft, scrollWidth, clientWidth } = target
|
||||
const maxScroll = Math.max(0, scrollWidth - clientWidth)
|
||||
const epsilon = 2
|
||||
|
||||
featuredAtStart.value = scrollLeft <= epsilon
|
||||
featuredAtEnd.value = scrollLeft >= maxScroll - epsilon
|
||||
}
|
||||
|
||||
watch(featuredTrackRef, (el) => {
|
||||
unbindFeaturedScroll()
|
||||
unbindFeaturedScroll = () => {}
|
||||
|
||||
if (!import.meta.client || !el) {
|
||||
updateFeaturedScrollEdges(null)
|
||||
return
|
||||
}
|
||||
|
||||
const onScroll = () => {
|
||||
updateFeaturedScrollEdges(el)
|
||||
}
|
||||
|
||||
onScroll()
|
||||
el.addEventListener('scroll', onScroll, { passive: true })
|
||||
const resizeObserver = new ResizeObserver(onScroll)
|
||||
resizeObserver.observe(el)
|
||||
unbindFeaturedScroll = () => {
|
||||
el.removeEventListener('scroll', onScroll)
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
watch(featuredPosts, () => {
|
||||
if (!import.meta.client) {
|
||||
return
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
updateFeaturedScrollEdges(featuredTrackRef.value)
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!import.meta.client) {
|
||||
@@ -107,6 +168,7 @@ onBeforeUnmount(() => {
|
||||
return
|
||||
}
|
||||
|
||||
unbindFeaturedScroll()
|
||||
document.removeEventListener('pointerdown', onDocumentPointerDown)
|
||||
})
|
||||
|
||||
@@ -120,6 +182,14 @@ const scrollFeatured = (direction) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (direction === 'left' && featuredAtStart.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (direction === 'right' && featuredAtEnd.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const firstCard = featuredTrackRef.value.querySelector('[data-featured-slide]')
|
||||
const cardWidth = firstCard ? firstCard.getBoundingClientRect().width : 244
|
||||
const gap = 24
|
||||
@@ -161,13 +231,29 @@ const scrollFeatured = (direction) => {
|
||||
<div class="flex items-end justify-between gap-2 border-b border-[var(--site-line)] pb-2">
|
||||
<h2 class="text-sm font-medium uppercase site-muted">Featured</h2>
|
||||
<div class="flex justify-between gap-2">
|
||||
<button class="cursor-pointer p-1 hover:opacity-75" type="button" aria-label="Previous" @click="scrollFeatured('left')">‹</button>
|
||||
<button class="cursor-pointer p-1 hover:opacity-75" type="button" aria-label="Next" @click="scrollFeatured('right')">›</button>
|
||||
<button
|
||||
class="featured-nav-prev cursor-pointer p-1 text-[var(--site-text)] hover:opacity-75 disabled:cursor-not-allowed disabled:opacity-35 disabled:hover:opacity-35"
|
||||
type="button"
|
||||
aria-label="Featured 이전"
|
||||
:disabled="featuredAtStart"
|
||||
@click="scrollFeatured('left')"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<button
|
||||
class="featured-nav-next cursor-pointer p-1 text-[var(--site-text)] hover:opacity-75 disabled:cursor-not-allowed disabled:opacity-35 disabled:hover:opacity-35"
|
||||
type="button"
|
||||
aria-label="Featured 다음"
|
||||
:disabled="featuredAtEnd"
|
||||
@click="scrollFeatured('right')"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="featuredTrackRef"
|
||||
class="mt-4 flex snap-x snap-mandatory gap-6 overflow-x-auto scroll-smooth pb-1 [--slides:1.4] sm:[--slides:1.6] lg:[--slides:2.6] [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
class="featured-posts-track mt-4 flex snap-x snap-mandatory gap-6 overflow-x-auto overscroll-x-contain scroll-smooth pb-1 touch-pan-x [-webkit-overflow-scrolling:touch] [--slides:1.4] sm:[--slides:1.6] lg:[--slides:2.6] [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
>
|
||||
<NuxtLink
|
||||
v-for="post in featuredPosts"
|
||||
|
||||
Reference in New Issue
Block a user