关键词:scroll absolute shadow ResizeObserver
最终效果
需求
设计师有这样一个需求,当有一个滚动列表时,用一个内阴影来提示用户某个方向能滚动。
下面的列表没有任何提示,用户不清楚是否有更多内容:
所以如果下面还有更多内容时,出现一个内阴影来提示用户:
如果列表向下滚动,则上方也出现一个内阴影:
问题一:在滚动容器内设置绝对定位不符合预期
首先先绘制 UI,使用 ::before ::after 来绘制内阴影:
<div class='container'>
<div
v-for='item in list'
:key='item.id'
class='item'
>
{{ item.title }}
</div>
</div>
.container {
position: relative;
overflow: auto;
width: 200px;
height: 200px;
border-radius: 4px;
&::before,
&::after {
content: '';
position: absolute;
left: 0;
right: 0;
height: 10px;
width: 100%;
pointer-events: none;
}
&::before {
top: 0;
background-image: linear-gradient(180deg, rgba(0, 0, 0, 0.2), transparent);
}
&::after {
bottom: 0;
background-image: linear-gradient(0deg, rgba(0, 0, 0, 0.2), transparent);
}
}
.item {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 32px;
}
运行下来发现,阴影居然跟着子元素一起滚动了。
为此得再添加一个容器来单独存放伪类元素。
<div class='container'>
<div class='scroll'> /* 额外添加一个容器 */
<div
v-for='item in list'
:key='item.id'
class='item'
>
{{ item.title }}
</div>
</div>
</div>
.scroll {
overflow: auto; // 把 .container 的滚动设置移到这里
width: 100%;
height: 100%;
}
问题二:如何监听滚动内容尺寸的变化
监听元素尺寸变化的方法通常使用 ResizeObserver 对象,但是现在缺少一个能体现滚动内容高度的容器。因为 div.scroll 虽然是滚动容器,但是它自身的高度是固定的。
所以我们还得加一个容器,这个容器在 div.scroll 内,它的高度由子元素撑开。
添加一个 div.list :
<div class='container'>
<div class='scroll'>
<div class='list'>
<div
v-for='item in list'
:key='item.id'
class='item'
>
{{ item.title }}
</div>
</div>
</div>
</div>
逻辑部分
接下来我们编写逻辑,我们需要在三个时机更新阴影显示状态:
- 列表首次渲染
- 尺寸发生变化
- 滚动时
// 列表首次渲染 和 尺寸发生变化
onMounted(() => {
const observer = new ResizeObserver(() => {
checkShadowShowStatus()
})
observer.observe(listRef.value)
})
/* 滚动时 */
<div class='container'>
<div
class='scroll'
@scroll='checkShadowShowStatus'
>
<div class='list'>
<div
v-for='item in list'
:key='item.id'
class='item'
>
{{ item.title }}
</div>
</div>
</div>
</div>
总结
代码 最后附上所有代码:
<template>
<div
:class='["container", {
"top-shadow": showTopShadow,
"bottom-shadow": showBottomShadow
}]'
>
<div
ref='scrollRef'
class='scroll'
@scroll='checkShadowShowStatus'
>
<div
ref='listRef'
class='list'
>
<div
v-for='item in list'
:key='item.id'
class='item'
>
{{ item.title }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
const list = ref(
Array(3).fill(null).map((item, index) => ({
id: index,
title: `Item ${index + 1}`,
})),
)
setTimeout(() => {
list.value = Array(20).fill(null).map((item, index) => ({
id: index,
title: `Item ${index + 1}`,
}))
}, 2000)
const scrollRef = ref(null)
const listRef = ref(null)
const showTopShadow = ref(false)
const showBottomShadow = ref(false)
onMounted(() => {
const observer = new ResizeObserver(() => {
checkShadowShowStatus()
})
observer.observe(listRef.value)
})
function checkShadowShowStatus () {
const {
scrollTop,
scrollHeight,
clientHeight,
} = scrollRef.value
showTopShadow.value = scrollTop > 0
showBottomShadow.value = scrollTop + clientHeight < scrollHeight
}
</script>
<style
lang='scss'
scoped
>
.container {
position: relative;
width: 200px;
height: 200px;
overflow: hidden;
border-radius: 4px;
&.top-shadow::before,
&.bottom-shadow::after {
content: '';
position: absolute;
left: 0;
right: 0;
height: 10px;
width: 100%;
pointer-events: none;
}
&.top-shadow::before {
top: 0;
background-image: linear-gradient(180deg, rgba(0, 0, 0, 0.2), transparent);
}
&.bottom-shadow::after {
bottom: 0;
background-image: linear-gradient(0deg, rgba(0, 0, 0, 0.2), transparent);
}
}
.scroll {
overflow: auto;
width: 100%;
height: 100%;
}
.item {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 32px;
}
</style>