CSS进阶实战3:平滑滚动、禁止滚动、监听滚动

平滑滚动:scrollIntoView、react-scroll

禁止滚动:桌面端禁止滚动,移动端禁止滚动

监听滚动:IntersectionObserver


参考课程:Build Responsive Real-World Websites with HTML and CSS
视频 | 源码

基于React的响应式单页应用开发、优化和部署


平滑滚动

实现平滑且可被浏览器兼容的滚动

使用场景:

点击导航栏连接,产生一个动画平滑地滚动到指定位置

image-20220511171619128

一般方式

导航栏中的按钮为a标签实现的超链接

1
<a href="#cta" class="btn btn--full margin-right-sm">Start eating well</a>

js脚本中选中所有a: link标签,手动绑定点击事件

  • 阻止默认行为
  • 获取每个a标签的href属性,得到转跳目标元素的id
  • 获取目标元素,调用scrollIntoView方法,设置参数behavior: "smooth",即可平滑滚动

但是该方法不适用于React的设计逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const allLinks = document.querySelectorAll("a:link");

allLinks.forEach(function (link) {
link.addEventListener("click", function (e) {
e.preventDefault();
const href = link.getAttribute("href");

// Scroll back to top
if (href === "#")
window.scrollTo({
top: 0,
behavior: "smooth",
});

// Scroll to other links
if (href !== "#" && href.startsWith("#")) {
const sectionEl = document.querySelector(href);
sectionEl.scrollIntoView({ behavior: "smooth" });
}

// Close mobile naviagtion
if (link.classList.contains("main-nav-link"))
headerEl.classList.toggle("nav-open");
});
});

其他方式参考:实现页面平滑滚动的N种姿势

React-scroll

使用React插件react-scroll

使用例:https://codesandbox.io/s/basic-6t84k

常用API

1
import { Link, animateScroll as scroll} from 'react-scroll'
参数 用例 含义
activeClass ‘active’ 判断滚动到指定的元素后,会在Link元素上加上一个css class,一般设类名为active
to ‘cta’ 目标元素id或name
spy true 判断滚动到指定的元素后,使对应的Link元素变成选中状态(就是加上activeClass)
hashSpy true 判断滚动到指定的元素后,在网页url后面显示哈希路径,例如 localhost:3000/#meals
smooth true 是否平滑
offset -80 额外滚动距离,单位为px,传参时直接传数值
duration 500 持续时间,单位为ms

animateScroll as scroll

源码

options

1
2
3
4
5
6
7
8
{
duration: 1500,
delay: 100,
smooth: true,
containerId: 'ContainerElementID',
offset: 50, // Scrolls to element + 50 pixels down the page
...
}

Scroll To Top

1
scroll.scrollToTop(options);

Scroll To Bottom

1
scroll.scrollToBottom(options);

Scroll To (position)

1
scroll.scrollTo(100, options);

Scroll To (Element)

1
scroll.scrollTo(element, options);

Scroll More (px)

1
scroll.scrollMore(10, options);

入参:

  • className:给a标签加上class
  • to:目标元素id
  • onClick:除了转跳意外的点击事件处理函数
  • props.children
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { Link, animateScroll as scroll } from 'react-scroll'

const NavLink = (props) => {
return (
// <a className='main-nav-link' href="#how">How it works</a>
props.to !== "#"
?
(<Link
activeClass="active"
className={props.className}
to={props.to}
smooth={true}
duration={500}
offset={-40}
onClick={props.onClick}>
{props.children}
</Link>)
:
<a className={props.className}
href="/#"
onClick={() => scroll.scrollToTop({ duration: 500 })}>
{props.children}
</a>
)
}

export default NavLink

使用:

1
<NavLink className='main-nav-link' to="how" onClick={onCloseHandle}>How it works</NavLink>

问题

spy和hashSpy会出现滚动到指定位置,但是判定激活的是上面的当前元素上面的那个元素,目前没有找到解决方案

参考:https://github.com/fisshy/react-scroll/issues/422

I solved this by letting react-waypoint do the “set active” handling instead. So I removed spy and onSetActive from my <Link> elements and wrapped each targeted element with <Waypoint>. That way you get more control of when the target element is activated, and you can offset the triggers freely with using waypoints, while still keeping any scroll offset set in the <Link> element.

阻止滚动

场景:

产生弹窗或打开导航栏时,不希望下层的页面能够滚动,需要手动禁用

image-20220513195237097

桌面端

粗暴的方法

html或body元素的overflow设为hidden(测试后是加在html元素上有效)

1
document.documentElement.style.overflow = 'hidden'

问题:

这样设置会影响子元素的overflow属性,造成页面布局抖动,不好

其他解决方案

参考阻止滚轮滚动事件

监听鼠标滚轮滚动mousewheel事件,阻止默认行为

问题:

用户仍然可以拖动滚动条进行操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const preventScroll = useCallback((e) => {
e.preventDefault()
}, [])
useEffect(() => {
if (isNavOpen) {

document.documentElement.addEventListener(
'mousewheel',
preventScroll,
{ passive: false }
); //passive 参数不能省略,用来兼容ios和android

} else {

document.documentElement.removeEventListener(
'mousewheel',
preventScroll
);

}
}, [isNavOpen, preventScroll])

参考JS-设置弹窗时候禁用滚动条

可以获取滚动条宽度,设置html元素 overflow: hidden 的同时,给它一个 margin-right,这样就可以避免抖动

移动端

考虑兼容性的方法:touchmove事件,阻止默认行为

原理:参考 React禁止页面滚动踩坑实践与方案梳理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const preventScroll = useCallback((e) => {
e.preventDefault()
}, [])
useEffect(() => {
if (isNavOpen) {

document.documentElement.addEventListener(
'touchmove',
preventScroll,
{ passive: false }
); //passive 参数不能省略,用来兼容ios和android

} else {

document.documentElement.removeEventListener(
'touchmove',
preventScroll
);

}
}, [isNavOpen, preventScroll])

监听滚动

场景:监听页面滚动,如果滚动到指定位置触发事件

IntersectionObserver

文档:https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver

IntersectionObserver接口 (从属于Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)。

可以用于检测某一元素是否滚动到窗口内,或者在窗口类滚动到的比例

API

使用new 关键字调用 IntersectionObserver 接口,返回一个新的IntersectionObserver对象,当其监听到目标元素的可见部分穿过了一个或多个**阈(thresholds)**时,会执行指定的回调函数。

1
var observer = new IntersectionObserver(callback[, options]);

入参:

  • 回调函数,当元素可见比例超过指定阈值后,会调用该回调函数,此回调函数接受两个参数:

  • options 可选

    一个可以用来配置observer实例的对象。如果options未指定,observer实例默认使用文档视口作为root,并且没有margin,阈值为0%(意味着即使一像素的改变都会触发回调函数)。可以指定以下配置:

    • root

      监听元素的祖先元素Element对象,其边界盒将被视作视口。目标在根的可见区域的的任何不可见部分都会被视为不可见。

    • rootMargin

      一个在计算交叉值时添加至根的边界盒(bounding_box (en-US))中的一组偏移量,类型为字符串(string) ,可以有效的缩小或扩大根的判定范围从而满足计算需要。语法大致和CSS 中的margin 属性等同; 可以参考 The root element and root margin in Intersection Observer API来深入了解margin的工作原理及其语法。默认值是”0px 0px 0px 0px”。

    • threshold

      规定了一个监听目标与边界盒交叉区域的比例值,可以是一个具体的数值或是一组0.0到1.0之间的数组。若指定值为0.0,则意味着监听元素即使与根有1像素交叉,此元素也会被视为可见. 若指定值为1.0,则意味着整个元素都在可见范围内时才算可见。可以参考Thresholds in Intersection Observer API 来深入了解阈值是如何使用的。阈值的默认值为0.0。

实例

以下使用场景:

  • 检测某一元素(section-hero)是否滚动到视窗内,滚动则在body元素上绑定新css属性(’sticky’)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const Hero = () => {
const heroRef = useRef(null);
const scrollObserver = useCallback(
node => {
new IntersectionObserver(entries => {
let en = entries[0]
console.log(en.isIntersecting)
if (!en.isIntersecting) {
// 不相交,则sticky
document.body.classList.add('sticky')

} else {
document.body.classList.remove('sticky')
}

}, {
root: null,
threshold: 0,
rootMargin: '-80px',
}).observe(node);
},
[]
);
useEffect(() => {
if (heroRef.current) {
scrollObserver(heroRef.current);
}
}, [scrollObserver, heroRef]);

return (
<section ref={heroRef} className="section-hero">
...
</section>
)
}
------ 本文结束 ❤ 感谢你的阅读 ------
------ 版权信息 ------

本文标题:CSS进阶实战3:平滑滚动、禁止滚动、监听滚动

文章作者:Lury

发布时间:2022年05月12日 - 21:46

最后更新:2022年05月20日 - 11:33

原始链接:https://luryzhu.github.io/2022/05/12/CSS/omnifood3_scroll/

许可协议:署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。