小教程:React 创建支持切换效果的导航组件

需求

需求不复杂,如上图所示:

  1. 导航栏,里面有若干个链接按钮
  2. 点击切换的时候,会有一个高亮标记随着选中的按钮移动
  3. 按钮尺寸不确定,高亮标记的尺寸随着选中的按钮变化

选择技术方案

我的第一想法是 CSS 纯原生,这样使用起来会非常简单。但是考察之后,发现不太可行,难点在于:

  1. 高亮标记如果跟着按钮走,无法处理按钮间切换
  2. 如果放在父容器里,可以在按钮间切换,但是尺寸没法跟着按钮走

所以短暂思考之后,我只能退而求其次,选择和框架结合的开发方式:高亮标记使用绝对定位来控制位置;当用户切换导航(点击按钮)的时候,使用 JS 获取目标按钮的位置和尺寸,并且赋给高亮标记;使用 CSS 变量控制动画,以提升代码的复用性,确保 CSS 规则不要太死硬。

这个过程并不复杂,我们可以利用事件冒泡机制,获取被点击的按钮,然后利用 getBoundingClientRect() 计算坐标。

CSS

CSS 部分比较简单,我们用一个伪元素作为高亮标记,然后配合 CSS 变量移动它即可。唯一需要注意的是,伪元素要跟按钮重合,所以必须用到 z-index,按钮也必须手动设置成 position: relative

(WP 代码高亮插件挂了,大家将就看……)

.slide-navigator {
--highlight-x: 0; /* 标记的 x 坐标 */
--highlight-width: 0; /* 标记的宽度 */
position: relative;

&::before {
content: '';
position: absolute;
z-index: 0;
bottom: 0;
left: var(--highlight-x);
width: var(--highlight-width);
background: #ffd850;
transition: all 0.2s;
}

> * {
position relative;
}
}

这里暂时没考虑响应式,如果要切换横纵,可以增加一个 Y 坐标。如果需要定制样式,可以将高亮标记的样式抽出来单独处理。

React JSX 组件

作为组件,导航里面的按钮要跟着业务逻辑走,最好在业务组件里生成,然后通过 props.children 传进来。点击事件可以直接交由父容器,也就是本组件侦听,然后利用 event.target 来判断选中的是哪个按钮。

这里还有一个抉择,即怎么确定高亮按钮是哪个。我考虑了一下,觉得将这个逻辑一并放在业务组件里比较好,然后通过 props.currentIndex 传进来使用。这样能确保本组件的复用性。坏处就是每个用到这个组件的地方都要写一个函数来判断,略嫌麻烦。

最终组件代码如下:

(WP 代码高亮插件挂了,大家将就看……)

import {
CSSProperties,
FC,
MouseEventHandler,
useEffect,
useRef,
useState
} from 'react';

interface SlideHighlightProps {
children: React.ReactNode;
className: string;
currentIndex: number;
}

type SlideNavigatorHighlight = CSSProperties & {
'--highlight-x'?: string;
'--highlight-width'?: string;
};

const SlideHighlight: FC<SlideHighlightProps> = function (props) {
const { className, children, currentIndex } = props;
const root = useRef<HTMLDivElement>(null);
const [navStyle, setNavStyle] = useState<SlideNavigatorHighlight>();

/* 处理按钮点击事件,找到被点击的按钮,并将其位置和宽度赋给高亮标记 */
const onClick: MouseEventHandler<HTMLDivElement> = (event) => {
if (!root.current) return;

const target = Array.from(root.current.children).find((v) =>
v.contains(event.target as Node)
) as HTMLElement;
const { left } = root.current.getBoundingClientRect();
const { left: l, width } = target.getBoundingClientRect();
setNavStyle({
'--highlight-x': `${l - left}px`,
'--highlight-width': `${width}px`
});
};

/* 组件初始化时,或者导航切换时,改变高亮标记的位置 */
useEffect(() => {
if (!root.current) return;

/* 如果导航不应该高亮,就把标记隐藏起来 */
if (currentIndex === -1) {
setNavStyle({
'--highlight-width': `0px`
});
return;
}

const { left } = root.current.getBoundingClientRect();
const target = root.current.children[currentIndex] as HTMLElement;
const { left: l, width } = target.getBoundingClientRect();
setNavStyle({
'--highlight-x': `${l - left}px`,
'--highlight-width': `${width}px`
});
}, [currentIndex]);

return (
<div ref={root} className={'slide-navigator ' + className} style={navStyle} onClick={onClick}>
{children}
</div>
);
};

export default SlideHighlight;

总结

我的 React 经验不太多,不考虑项目结构倒也问题不大,但是抽象组件的经验比较少,这次也是在 ChatGPT 帮助下才写好,所以赶紧趁热发一篇笔记。

如果你对 React 经验比较丰富,发现我上面的代码有问题,欢迎指出。如果你对 React 开发有问题,对组件开发有疑问,也欢迎留言讨论。

如果您觉得文章内容对您有用,不妨支持我创作更多有价值的分享:


已发布

分类

来自

评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据