标签: react.fc

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

    小教程: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 开发有问题,对组件开发有疑问,也欢迎留言讨论。