这是我们的“创建完美轮播”教程系列的第三部分,也是最后一部分。 在第1部分中 ,我们评估了Netflix和Amazon(世界上使用量最大的两个轮播)上的轮播。 我们设置了旋转木马并实现了触摸滚动。
然后在第2部分中 ,我们添加了水平鼠标滚动,分页和进度指示器。 繁荣。
现在,在最后一部分中,我们将研究键盘可访问性这个阴暗且经常被遗忘的世界。 当视口大小更改时,我们将调整代码以重新测量轮播。 最后,我们将使用Spring物理技术进行一些最后的润色。
您可以使用此CodePen继续学习 。
键盘辅助功能
的确,大多数用户不依赖键盘导航,因此可悲的是我们有时会忘记使用它的用户。 在某些国家/地区,无法访问网站可能是非法的 。 但更糟糕的是,这是一个鸡巴的动作。
好消息是它通常很容易实现! 实际上,浏览器为我们完成了大部分工作。 认真地:尝试浏览我们制作的轮播。 因为我们已经使用了语义标记,所以您已经可以!
除非您注意到,否则我们的导航按钮会消失。 这是因为浏览器不允许将焦点集中在视口之外的元素上。 因此,即使我们有overflow: hidden
集,我们也无法水平滚动页面; 否则,页面确实会滚动以显示具有焦点的元素。
没关系,在我看来,它虽然不十分令人满意,但却可以称为“可使用的”。
Netflix的轮播也以这种方式工作。 但是,由于它们的大多数标题都是延迟加载的,而且它们还可以通过键盘被动访问(这意味着他们没有编写任何专门用于处理该代码的代码),因此我们实际上只能选择少数几个标题,已经加载。 它看起来也很糟糕:
我们可以做得更好。
处理focus
事件
为此,我们将收听轮播中任何项目触发的focus
事件。 当一个项目获得焦点时,我们将查询它的位置。 然后,我们将针对sliderX
和sliderVisibleWidth
进行检查,以查看该项目是否在可见窗口内。 如果不是,我们将使用在第2部分中编写的相同代码对它进行分页。
在carousel
功能的末尾,添加以下事件侦听器:
slider.addEventListener('focus', onFocus, true);
您会注意到我们提供了第三个参数true
。 除了可以将事件侦听器添加到每个项目外,我们还可以使用所谓的事件委托 仅在一个元素上监听事件,它们的直接父元素。 的focus
事件不起泡,所以true
是告诉事件侦听器来侦听捕获阶段,其中来自每个元件上的事件触发阶段window
通过与靶(在这种情况下,项目接收焦点)。
在我们不断增长的事件侦听器块上方,添加onFocus
函数:
function onFocus(e) {
}
在本节的其余部分中,我们将使用此功能。
我们需要测量该项目的left
和right
偏移和检查是否要么点位于当前可视区域之外。
该项目由事件的target
参数提供,我们可以使用getBoundingClientRect
对其进行测量:
const { left, right } = e.target.getBoundingClientRect();
left
和right
相对于视口 ,而不是滑块。 因此,我们需要获得轮播容器的left
偏移量以解决此问题。 在我们的示例中,该0
,但是为了使旋转木马更坚固,应考虑将其放置在任何位置。
const carouselLeft = container.getBoundingClientRect().left;
现在,我们可以做一个简单的检查,以查看该项目是否在滑块的可见区域之外并朝该方向分页:
if (left < carouselLeft) {
gotoPrev();
} else if (right > carouselLeft + sliderVisibleWidth) {
gotoNext();
}
现在,当我们四处切换时,轮播会充满信心地通过键盘焦点分页! 只需几行代码即可向我们的用户展示更多的爱。
重新测量轮播
在学习本教程时,您可能已经注意到,如果您调整浏览器视口的大小,则轮播将无法再正确地分页。 这是因为在初始化时,我们仅测量了其相对于可见区域的宽度。
为了确保轮播正常运行,我们需要用一个新的事件侦听器替换一些测量代码,该事件侦听器会在调整window
大小时触发。
现在,在carousel
功能的开始附近, carousel
我们定义progressBar
的那一行之后,我们希望用let
替换其中三个const
度量,因为我们将在视口更改时更改它们:
const totalItemsWidth = getTotalItemsWidth(items);
const maxXOffset = 0;
let minXOffset = 0;
let sliderVisibleWidth = 0;
let clampXOffset;
然后,我们可以将先前计算这些值的逻辑移到新的measureCarousel
函数中:
function measureCarousel() {
sliderVisibleWidth = slider.offsetWidth;
minXOffset = - (totalItemsWidth - sliderVisibleWidth);
clampXOffset = clamp(minXOffset, maxXOffset);
}
我们想立即调用此函数,因此我们仍在初始化时设置这些值。 在下一行,调用measureCarousel
:
measureCarousel();
转盘应该完全像以前一样工作。 要更新窗口大小,我们只需在carousel
功能的最后添加此事件侦听器:
window.addEventListener('resize', measureCarousel);
现在,如果您调整轮播的大小并尝试进行分页,它将继续按预期工作。
性能说明
值得考虑的是,在现实世界中,您可能在同一页面上有多个轮播,从而使此度量代码的性能影响乘以该数量。
正如我们在第2部分中简要讨论的那样,比您必须执行更多次频繁的计算是不明智的。 对于指针和滚动事件,我们说过要每帧执行一次,以保持60fps。 调整大小事件稍有不同,整个文档将重排,这可能是网页遇到的最耗费资源的时刻。
在用户调整窗口大小之前,我们不需要重新测量轮播,因为在此之前他们不会与之交互。 我们可以换我们的measureCarousel
在被称为反跳一种特殊的功能函数。
防反跳函数本质上说:“仅在x
毫秒内未调用此函数时,才触发它。” 您可以在David Walsh的出色入门文章中了解有关防抖动的更多信息,并且还可以获取一些示例代码。
画龙点睛
到目前为止,我们已经创建了一个很好的轮播。 它易于访问,动画效果很好,可以在触摸和鼠标上使用,并且以本机滚动轮播不允许的方式提供了很大的设计灵活性。
但这不是“创建漂亮的轮播”教程系列。 是时候让我们炫耀一点了,为此,我们有了秘密武器。 泉水 。
我们将使用spring添加两个交互。 一种用于触摸,另一种用于分页。 它们都将以一种有趣和有趣的方式让用户知道他们已经到达轮播的尽头。
触摸弹簧
首先,让我们在用户尝试将滑块滑过其边界时添加一个iOS风格的拖船。 目前,我们正在使用clampXOffset
限制触摸滚动。 取而代之的是,让我们用一些在计算出的偏移量超出其边界时应用拖船的代码来代替它。
首先,我们需要导入弹簧。 有一个变压器称为nonlinearSpring
它适用对我们提供它的数量呈指数增长的力量,朝着origin
。 这意味着我们将滑块拉得越远,拉回就越多。 我们可以这样导入它:
const { applyOffset, clamp, nonlinearSpring, pipe } = transform;
在determineDragDirection
函数中,我们具有以下代码:
action.output(pipe(
({ x }) => x,
applyOffset(action.x.get(), sliderX.get()),
clampXOffset,
(v) => sliderX.set(v)
));
在其上方,让我们创建两个弹簧,每个旋转木马滚动限制一个弹簧:
const elasticity = 5;
const tugLeft = nonlinearSpring(elasticity, maxXOffset);
const tugRight = nonlinearSpring(elasticity, minXOffset);
确定elasticity
值是一个反复试验并了解正确感觉的问题。 数字太小,弹簧感觉太硬。 太高了,您将不会注意到它的拉力,或者更糟的是,它会将滑块推离用户的手指更远!
现在,我们只需要编写一个简单的函数,如果提供的值超出允许范围,则将应用这些弹簧之一:
const applySpring = (v) => {
if (v > maxXOffset) return tugLeft(v);
if (v < minXOffset) return tugRight(v);
return v;
};
我们可以用clampXOffset
替换上面代码中的applySpring
。 现在,如果将滑块拉出其边界,它将向后拉!
但是,当我们放开弹簧时,它会毫不客气地恢复原状。 我们想要修改我们当前处理动量滚动的stopTouchScroll
函数,以检查滑块是否仍在允许范围之外,如果是,则使用physics
作用来施加弹簧。
Spring物理
physics
动作也能够对弹簧建模。 我们只需要为它提供spring
和to
性能。
在stopTouchScroll
,将现有的滚动physics
初始化移动到逻辑上,以确保我们在滚动限制之内:
const currentX = sliderX.get();
if (currentX < minXOffset || currentX > maxXOffset) {
} else {
action = physics({
from: currentX,
velocity: sliderX.getVelocity(),
friction: 0.2
}).output(pipe(
clampXOffset,
(v) => sliderX.set(v)
)).start();
}
在if
语句的第一个子句中,我们知道滑块超出了滚动限制,因此可以添加spring:
action = physics({
from: currentX,
to: (currentX < minXOffset) ? minXOffset : maxXOffset,
spring: 800,
friction: 0.92
}).output((v) => sliderX.set(v))
.start();
我们要创建一个弹簧,感觉敏捷和反应灵敏。 我选择了一个较高的spring
值来产生紧密的“砰”声,并且我将friction
降低到0.92
以允许有一些弹跳。 您可以将其设置为1
以完全消除反弹。
作为一项家庭作业,请尝试使用在x偏移量达到其边界时会触发类似弹簧的函数来替换滚动physics
output
函数中的clampXOffset
。 而不是当前的突然停止,请尝试使其在末尾轻轻反弹。
分页春天
触摸用户总能得到春天的好处,对吗? 让我们通过检测轮播何时处于滚动限制位置,并进行指示性拖船来清楚,自信地向用户显示他们的末日,将这种爱分享给桌面用户。
首先,我们要在达到限制时禁用分页按钮。 首先,我们添加一个CSS规则,为按钮设置样式,以显示按钮已被disabled
。 在button
规则中,添加:
transition: background 200ms linear;
&.disabled {
background: #eee;
}
我们在这里使用的是类,而不是更具语义的disabled
属性,因为我们仍然想捕获单击事件,顾名思义, disabled
会阻塞。
将此disabled
类添加到“上一个”按钮,因为每个轮播的起点都是0
偏移:
<button class="prev disabled">Prev</button>
在carousel
的顶部,创建一个名为checkNavButtonStatus
的新函数。 我们希望此函数仅针对minXOffset
和maxXOffset
检查提供的值,并maxXOffset
设置按钮disabled
类:
function checkNavButtonStatus(x) {
if (x <= minXOffset) {
nextButton.classList.add('disabled');
} else {
nextButton.classList.remove('disabled');
if (x >= maxXOffset) {
prevButton.classList.add('disabled');
} else {
prevButton.classList.remove('disabled');
}
}
}
每次sliderX
更改时sliderX
调用它。 如果这样做的话,每当弹簧围绕滚动边界摆动时,按钮就会开始闪烁。 这也会导致怪异 行为,如果在这些Spring动画之一中按下了其中一个按钮。 即使我们在轮播的末端,“滚动结束”拖船也应始终触发,即使有弹簧动画将其从绝对末端拉开。
因此,我们需要对何时调用此函数有更多的选择。 称之为明智的做法:
在onWheel
的最后一行,添加checkNavButtonStatus(newX);
。
在goto
的最后一行,添加checkNavButtonStatus(targetX);
。
最后,在结束determineDragDirection
,势头滚动条(内部代码在else
的) stopTouchScroll
,更换:
(v) => sliderX.set(v)
带有:
(v) => {
sliderX.set(v);
checkNavButtonStatus(v);
}
现在剩下的就是修改gotoPrev
和gotoNext
来检查其触发按钮的classList是否已disabled
并且如果不存在则仅进行分页:
const gotoNext = (e) => !e.target.classList.contains('disabled')
? goto(1)
: notifyEnd(-1, maxXOffset);
const gotoPrev = (e) => !e.target.classList.contains('disabled')
? goto(-1)
: notifyEnd(1, minXOffset);
notifyEnd
函数只是另一个physics
弹簧,它看起来像这样:
function notifyEnd(delta, targetOffset) {
if (action) action.stop();
action = physics({
from: sliderX.get(),
to: targetOffset,
velocity: 2000 * delta,
spring: 300,
friction: 0.9
})
.output((v) => sliderX.set(v))
.start();
}
尝试一下,然后根据自己的喜好调整physics
参数。
仅剩一个小错误。 当滑块弹跳到最左侧边界时,进度条将反转。 我们可以通过替换以下内容快速解决此问题:
progressBarRenderer.set('scaleX', progress);
带有:
progressBarRenderer.set('scaleX', Math.max(progress, 0));
我们可以 防止它弹跳,但是就我个人而言,它反映了弹簧运动是很酷的。 当它从里到外翻转时,看起来只是很奇怪。
自己清理后
使用单页应用程序时,网站在用户会话中的持续时间更长。 通常,即使“页面”发生变化,我们仍在运行与初始加载相同的JS运行时。 我们不能每次用户单击链接时都依赖整洁的表述,这意味着我们必须自己清理一下,以防止事件侦听器触发失效的元素。
在React中,此代码放置在componentWillLeave
方法中。 Vue使用beforeDestroy
。 这是一个纯JS实现,但我们仍然可以提供在两个框架中均能正常工作的destroy方法。
到目前为止,我们的carousel
功能尚未返回任何内容。 让我们改变一下。
首先,将最后一行(称为carousel
的行)更改为:
const destroyCarousel = carousel(document.querySelector('.container'));
我们将只从carousel
返回一件事,该函数解除了我们所有事件监听器的绑定。 在carousel
功能的最后,输入:
return () => {
container.removeEventListener('touchstart', startTouchScroll);
container.removeEventListener('wheel', onWheel);
nextButton.removeEventListener('click', gotoNext);
prevButton.removeEventListener('click', gotoPrev);
slider.removeEventListener('focus', onFocus);
window.removeEventListener('resize', measureCarousel);
};
现在,如果您呼叫destroyCarousel
并尝试与该旋转木马一起玩,则什么也不会发生! 这几乎有点难过,看到它这个样子。
那就是那
ew。 好多! 我们走了多远。 您可以在此CodePen上查看成品。 在最后一部分中,我们增加了键盘的可访问性,当视口改变时,对旋转木马进行了重新评估,对弹簧物理进行了一些有趣的添加,以及令人心碎但又需要将其全部拆除的令人沮丧的步骤。
我希望您和我一样喜欢本教程。 我很想听听您对我们可以改善可及性或增加更多趣味性的其他方式的想法。
翻译自: https://code.tutsplus.com/tutorials/create-the-perfect-carousel-part-3--cms-29636