これは掘金のウェブ版のヘッダーで、スクロールバーが下に滑ると、メインヘッダーが隠れ、サブヘッダーがページの上部に吸着します。
スタイル#
まず、2 つの div を使ってこの 2 つのヘッダーを模擬します。
<template>
<div class="main-header-wrapper">
<div class="main-header"></div>
</div>
<div class="sub-header-wrapper">
<div class="sub-header"></div>
</div>
<div v-for="i of 100" :key="i">スクロールテスト</div>
</template>
<style scoped>
.main-header-wrapper {
height: 50px;
}
.main-header {
height: 50px;
width: 100%;
background: #ff0000;
position: fixed;
top: 0;
}
.sub-header-wrapper {
height: 40px;
}
.sub-header {
height: 40px;
width: 100%;
background: #00ff00;
position: fixed;
top: 50px;
}
</style>
ここでは絶対位置を使用して 2 つのヘッダーをページの上部に固定しています。この 2 つの div はそれぞれmain-header
とsub-header
です。彼らはそれぞれコンテナmain-header-wrapper
とsub-header-wrapper
で包まれており、ヘッダーが文書フローから外れることによって本文要素を隠すのを防ぐためです。
簡略化#
スクロール方向の検出を一旦脇に置き、まずはこのような効果を実現しましょう:2 つのヘッダーにクラス名hidden
を追加すると、メインヘッダーが隠れ、サブヘッダーが吸着します。ここでは transform を使って実現できます。
効果を示すために、hidden
クラス名を追加および削除するためのボタンを追加しました。
<script setup>
import { ref } from 'vue';
const isHidden = ref(false);
</script>
<template>
<div class="main-header-wrapper">
<div class="main-header" :class="{'hidden':isHidden}"></div>
</div>
<div class="sub-header-wrapper">
<div class="sub-header" :class="{'hidden':isHidden}"></div>
</div>
<div v-for="i of 100" :key="i">スクロールテスト</div>
<button style="position:fixed;bottom: 10px; right: 10px;" @click="isHidden = !isHidden">
切り替え
</button>
</template>
<style>
html, body {
margin: 0;
padding: 0;
}
</style>
<style scoped>
.main-header-wrapper {
height: 50px;
}
.main-header {
height: 50px;
width: 100%;
background: #ff0000;
position: fixed;
top: 0;
}
.sub-header-wrapper {
height: 40px;
}
.sub-header {
height: 40px;
width: 100%;
background: #00ff00;
position: fixed;
top: 50px;
}
.main-header, .sub-header {
transition: transform .15s ease-in-out;
transform: translateY(0px);
}
.main-header.hidden, .sub-header.hidden {
transform: translateY(-50px);
}
</style>
選択子.main-header.hidden
と.sub-header.hidden
を使用して、hidden クラス名を持つヘッダー要素を選択し、transform プロパティをtranslateY(-50px)
(つまり上に 50px オフセット)に設定します。同時に要素に transition を設定し、イージング効果を提供します。
これで、ヘッダーの表示と非表示が実現されました。次はスクロール方向の検出です。
実装#
vueUse のuseScroll
とuseDebounceFn
を使用して、一部のコードを簡略化しました。
<script setup>
import { ref, watch } from 'vue';
import { useScroll, useDebounceFn } from '@vueuse/core'
const isHidden = ref(false);
const { directions, isScrolling, arrivedState } = useScroll(document)
const checkHeaderStatus = useDebounceFn(
(top, bottom, topArrived) => {
if (topArrived) {
isHidden.value = false
return
}
if (top) {
isHidden.value = false
} else if (bottom) {
isHidden.value = true
}
},
100
)
watch(directions, () => {
if (isScrolling.value) {
checkHeaderStatus(directions.top, directions.bottom, arrivedState.top)
}
})
</script>
useScroll は、反応的なスクロール方向、スクロール状態(上部 / 下部に到達)およびスクロール中かどうかを出力できます。
関数が返す directions は反応的なオブジェクトであり、これを監視します。この変数が変化すると、スクロール方向が変わったことを示します。
高頻度のトリガー(例えば、上下に繰り返しスクロールすること)によってヘッダーが乱れるのを防ぐために、防抖関数を使用して制限しました。その中のcheckHeaderStatus
関数内の
if (top) {
isHidden.value = false
} else if (bottom) {
isHidden.value = true
}
が重要です。上にスクロールするとヘッダーが表示され、下にスクロールするとヘッダーが隠れます。
このコードの上に、意味不明なコードがあります:
if (topArrived) {
isHidden.value = false
return
}
スクロールバーが上部に到達したとき、ヘッダーを直接表示します。これは、iOS デバイスの Safari ブラウザのバウンス効果による誤判定を避けるためです。
だから、なぜ Safari のバウンス効果もスクロールイベントをトリガーするのか!!!さらには y 値が負になることもある。
ここにはもう一つの問題があります。上部にスクロールしたときだけを検出し、下部にスクロールしたときは検出していません。これは、良い方法を思いつかなかったからです。
前者は y 値が 0(または 0 未満)であるかどうかを判断することで実現できますが、後者は通常、文書の高さ - ビューポートの高さがスクロールバーの y 値と一致するはずです。しかし、Safari では一致しない可能性があります。Safari では、アドレスバーが収縮すると、上記の公式が成り立ちますが、アドレスバーが展開されている状態では、両者はアドレスバーの高さの差が生じます。そして、アドレスバーが展開されているかどうかを判断する方法は見つかりませんでした。
もし、下部にスクロールしたかどうかを判断する方法を知っている方がいれば、ぜひ教えてください。